From 290e8e4129ab573bb5c9fe73fcac60367f6d0301 Mon Sep 17 00:00:00 2001 From: Tom Manley Date: Tue, 24 Nov 2015 14:03:29 -0600 Subject: [PATCH 1/6] The x,y,z attributes are often sent in a separate Attribute Report from the accelerometer attribute. Sometimes, however, they are all sent in the same Attribute Report. When that happens, only the accelerometer attribute was being handled and the x,y,z attributes were not. Now they are all handled if they arrive in the same message. Resolves: https://smartthings.atlassian.net/browse/DVCSMP-1271 --- .../smartsense-multi-sensor.groovy | 71 +++++++++++-------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy index 8d614c3..a4b670b 100644 --- a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy @@ -115,31 +115,30 @@ } } - def parse(String description) { - - Map map = [:] - if (description?.startsWith('catchall:')) { - map = parseCatchAllMessage(description) - } - else if (description?.startsWith('read attr -')) { - map = parseReportAttributeMessage(description) - } +def parse(String description) { + Map map = [:] + if (description?.startsWith('catchall:')) { + map = parseCatchAllMessage(description) + } else if (description?.startsWith('temperature: ')) { - map = parseCustomMessage(description) - } - else if (description?.startsWith('zone status')) { - map = parseIasMessage(description) - } + map = parseCustomMessage(description) + } + else if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } def result = map ? createEvent(map) : null - if (description?.startsWith('enroll request')) { - List cmds = enrollResponse() - log.debug "enroll response: ${cmds}" - result = cmds?.collect { new physicalgraph.device.HubAction(it) } - } - return result - } + if (description?.startsWith('enroll request')) { + List cmds = enrollResponse() + log.debug "enroll response: ${cmds}" + result = cmds?.collect { new physicalgraph.device.HubAction(it) } + } + else if (description?.startsWith('read attr -')) { + result = parseReportAttributeMessage(description).each { createEvent(it) } + } + return result +} private Map parseCatchAllMessage(String description) { Map resultMap = [:] @@ -178,28 +177,40 @@ private boolean shouldProcessMessage(cluster) { return !ignoredMessage } -private Map parseReportAttributeMessage(String description) { +private List parseReportAttributeMessage(String description) { Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> def nameAndValue = param.split(":") map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] } - - Map resultMap = [:] + + List result = [] if (descMap.cluster == "0402" && descMap.attrId == "0000") { def value = getTemperature(descMap.value) - resultMap = getTemperatureResult(value) + result << getTemperatureResult(value) } else if (descMap.cluster == "FC02" && descMap.attrId == "0010") { - resultMap = getAccelerationResult(descMap.value) + if (descMap.value.size() == 32) { + // value will look like 00ae29001403e2290013001629001201 + // breaking this apart and swapping byte order where appropriate, this breaks down to: + // X (0x0012) = 0x0016 + // Y (0x0013) = 0x03E2 + // Z (0x0014) = 0x00AE + // note that there is a known bug in that the x,y,z attributes are interpreted in the wrong order + // this will be fixed in a future update + def threeAxisAttributes = descMap.value[0..-9] + result << parseAxis(threeAxisAttributes) + descMap.value = descMap.value[-2..-1] + } + result << getAccelerationResult(descMap.value) } - else if (descMap.cluster == "FC02" && descMap.attrId == "0012") { - resultMap = parseAxis(descMap.value) + else if (descMap.cluster == "FC02" && descMap.attrId == "0012") { + result << parseAxis(descMap.value) } else if (descMap.cluster == "0001" && descMap.attrId == "0020") { - resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) + result << getBatteryResult(Integer.parseInt(descMap.value, 16)) } - return resultMap + return result } private Map parseCustomMessage(String description) { From 1b9d2fe9ce12250d0d6c8c4e5f9ca2cdc2b8bbd1 Mon Sep 17 00:00:00 2001 From: Warodom Khamphanchai Date: Wed, 28 Oct 2015 12:46:44 -0700 Subject: [PATCH 2/6] 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)} +} From c5da3fe4a0af76a922e48696eb2cde679af46f54 Mon Sep 17 00:00:00 2001 From: Warodom Khamphanchai Date: Fri, 30 Oct 2015 18:37:41 -0700 Subject: [PATCH 3/6] DVCSMP-535 - Fix activity feed displays unformatted text - update oauth/callback url, use getApiServerUrl() for proxying to corresponding shard --- .../ecobee-thermostat.groovy | 31 ++++++------------- .../ecobee-connect.src/ecobee-connect.groovy | 9 +++--- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy index 3a3140e..45e552c 100644 --- a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy +++ b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy @@ -139,36 +139,23 @@ def generateEvent(Map results) { def linkText = getLinkText(device) def isChange = false def isDisplayed = true + def event = [name: name, linkText: linkText, descriptionText: getThermostatDescriptionText(name, value, linkText), + handlerName: name] 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 - - sendEvent( - name: name, - value: sendValue, - unit: location.temperatureScale, - linkText: linkText, - descriptionText: getThermostatDescriptionText(name, value, linkText), - handlerName: name, - isStateChange: isChange, - displayed: isDisplayed) - - } else { + event << [value: sendValue, isStateChange: isChange, displayed: isDisplayed] + } else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){ + isChange = isStateChange(device, name, value.toString()) + event << [value: value.toString(), isStateChange: isChange, displayed: false] + } else { 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) - + event << [value: value.toString(), isStateChange: isChange, displayed: isDisplayed] } + sendEvent(event) } generateSetpointEvent () generateStatusEvent () diff --git a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy index a33037b..4386645 100644 --- a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy +++ b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy @@ -95,7 +95,7 @@ def oauthInitUrl() { redirect_uri: callbackUrl //"https://graph.api.smartthings.com/oauth/callback" ] - redirect(location: "https://api.ecobee.com/authorize?${toQueryString(oauthParams)}") + redirect(location: "${apiEndpoint}/authorize?${toQueryString(oauthParams)}") } def callback() { @@ -739,11 +739,12 @@ def sendJson(child = null, String jsonBody) { def getChildName() { "Ecobee Thermostat" } def getSensorChildName() { "Ecobee Sensor" } -def getServerUrl() { getApiServerUrl() } -def getSmartThingsClientId() { appSettings.clientId } +def getServerUrl() { return "https://graph.api.smartthings.com" } +def getShardUrl() { return getApiServerUrl() } def getCallbackUrl() { "https://graph.api.smartthings.com/oauth/callback" } -def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}" } +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) { From e6367a78327ae4cb361e684bec06000ad970d9e6 Mon Sep 17 00:00:00 2001 From: Warodom Khamphanchai Date: Wed, 2 Dec 2015 17:29:46 -0800 Subject: [PATCH 4/6] Fix Ecobee access_token doesn't get refresh after it's expired --- .../ecobee-sensor.src/ecobee-sensor.groovy | 5 + .../ecobee-thermostat.groovy | 5 + .../ecobee-connect.src/ecobee-connect.groovy | 195 ++++++++++-------- 3 files changed, 123 insertions(+), 82 deletions(-) diff --git a/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy b/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy index b979cf9..e984bb6 100644 --- a/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy +++ b/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy @@ -64,3 +64,8 @@ void poll() { log.debug "Executing 'poll' using parent SmartApp" parent.pollChildren(this) } + +//generate custom mobile activity feeds event +def generateActivityFeedsEvent(notificationMessage) { + sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true) +} diff --git a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy index 45e552c..560adbf 100644 --- a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy +++ b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy @@ -676,3 +676,8 @@ def generateStatusEvent() { log.debug "Generate Status Event = ${statusText}" sendEvent("name":"thermostatStatus", "value":statusText, "description":statusText, displayed: true) } + +//generate custom mobile activity feeds event +def generateActivityFeedsEvent(notificationMessage) { + sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true) +} diff --git a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy index 4386645..790820d 100644 --- a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy +++ b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy @@ -10,14 +10,14 @@ * 10-28-2015 DVCSMP-604 - accessory sensor, DVCSMP-1174, DVCSMP-1111 - not respond to routines */ definition( - name: "Ecobee (Connect)", - namespace: "smartthings", - author: "SmartThings", - description: "Connect your Ecobee thermostat to SmartThings.", - category: "SmartThings Labs", - iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png", - singleInstance: true + name: "Ecobee (Connect)", + namespace: "smartthings", + author: "SmartThings", + description: "Connect your Ecobee thermostat to SmartThings.", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png", + singleInstance: true ) { appSetting "clientId" } @@ -236,27 +236,32 @@ def getEcobeeThermostats() { ] def stats = [:] - httpGet(deviceListParams) { resp -> + try { + httpGet(deviceListParams) { resp -> - if(resp.status == 200) { - resp.data.thermostatList.each { stat -> - atomicState.remoteSensors = stat.remoteSensors - def dni = [ app.id, stat.identifier ].join('.') - stats[dni] = getThermostatDisplayName(stat) - } - } else { - log.debug "http status: ${resp.status}" - //refresh the auth token - if (resp.status == 500 && resp.data.status.code == 14) { - log.debug "Storing the failed action to try later" - atomicState.action = "getEcobeeThermostats" - log.debug "Refreshing your auth_token!" - refreshAuthToken() - } else { - log.error "Authentication error, invalid authentication method, lack of credentials, etc." - } - } - } + if (resp.status == 200) { + resp.data.thermostatList.each { stat -> + atomicState.remoteSensors = stat.remoteSensors + def dni = [app.id, stat.identifier].join('.') + stats[dni] = getThermostatDisplayName(stat) + } + } else { + log.debug "http status: ${resp.status}" + //refresh the auth token + if (resp.status == 500 && resp.data.status.code == 14) { + log.debug "Storing the failed action to try later" + atomicState.action = "getEcobeeThermostats" + log.debug "Refreshing your auth_token!" + refreshAuthToken() + } else { + log.error "Authentication error, invalid authentication method, lack of credentials, etc." + } + } + } + } catch(Exception e) { + log.debug "___exception getEcobeeThermostats(): " + e + refreshAuthToken() + } atomicState.thermostats = stats return stats } @@ -338,22 +343,19 @@ def initialize() { atomicState.thermostatData = [:] //reset Map to store thermostat data + //send activity feeds to tell that device is connected + def notificationMessage = "is connected to SmartThings" + sendActivityFeeds(notificationMessage) + state.timeSendPush = null + pollHandler() //first time polling data data from thermostat //automatically update devices status every 5 mins runEvery5Minutes("poll") -} + //since access_token expires every 2 hours + runEvery1Hour("refreshAuthToken") -def uninstalled() { - log.info("Uninstalling, removing child devices...") - removeChildDevices(getChildDevices()) -} - -private removeChildDevices(delete) { - delete.each { - deleteChildDevice(it.deviceNetworkId) - } } def pollHandler() { @@ -579,65 +581,69 @@ def toQueryString(Map m) { } private refreshAuthToken() { - log.debug "refreshing auth token" + log.debug "refreshing auth token" - if(!atomicState.refreshToken) { - log.warn "Can not refresh OAuth token since there is no refreshToken stored" - } else { + if(!atomicState.refreshToken) { + log.warn "Can not refresh OAuth token since there is no refreshToken stored" + } else { - def refreshParams = [ - method: 'POST', - uri : apiEndpoint, - path : "/token", - query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId], - ] + def refreshParams = [ + method: 'POST', + uri : apiEndpoint, + path : "/token", + query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId], + ] - log.debug refreshParams + log.debug refreshParams - //changed to httpPost - try { - def jsonMap - httpPost(refreshParams) { resp -> + def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials." + //changed to httpPost + try { + def jsonMap + httpPost(refreshParams) { resp -> - if(resp.status == 200) { - log.debug "Token refreshed...calling saved RestAction now!" + if(resp.status == 200) { + log.debug "Token refreshed...calling saved RestAction now!" - debugEvent("Token refreshed ... calling saved RestAction now!") + debugEvent("Token refreshed ... calling saved RestAction now!") - log.debug resp + log.debug resp - jsonMap = resp.data + jsonMap = resp.data - if(resp.data) { + if(resp.data) { - log.debug resp.data - debugEvent("Response = ${resp.data}") + log.debug resp.data + debugEvent("Response = ${resp.data}") - atomicState.refreshToken = resp?.data?.refresh_token - atomicState.authToken = resp?.data?.access_token + atomicState.refreshToken = resp?.data?.refresh_token + atomicState.authToken = resp?.data?.access_token - debugEvent("Refresh Token = ${atomicState.refreshToken}") - debugEvent("OAUTH Token = ${atomicState.authToken}") + debugEvent("Refresh Token = ${atomicState.refreshToken}") + debugEvent("OAUTH Token = ${atomicState.authToken}") - if(atomicState.action && atomicState.action != "") { - log.debug "Executing next action: ${atomicState.action}" + if(atomicState.action && atomicState.action != "") { + log.debug "Executing next action: ${atomicState.action}" - "${atomicState.action}"() + "${atomicState.action}"() - //remove saved action - atomicState.action = "" - } + //remove saved action + atomicState.action = "" + } - } - atomicState.action = "" - } else { - log.debug "refresh failed ${resp.status} : ${resp.status.code}" - } - } - } catch(Exception e) { - log.debug "caught exception refreshing auth token: " + e - } - } + } + atomicState.action = "" + } else { + log.debug "refresh failed ${resp.status} : ${resp.status.code}" + } + } + } catch(Exception e) { + log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}" + if (e.statusCode == 401) { + sendPushAndFeeds(notificationMessage) + } + } + } } def resumeProgram(child, deviceId) { @@ -727,7 +733,7 @@ def sendJson(child = null, String jsonBody) { } catch(Exception e) { log.debug "Exception Sending Json: " + e debugEvent ("Exception Sending JSON: " + e) -// debugEventFromParent(child, "Exception Sending JSON: " + e) + refreshAuthToken() return false } @@ -761,3 +767,28 @@ def debugEvent(message, displayEvent = false) { 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){ + log.warn "sendPushAndFeeds >> notificationMessage: ${notificationMessage}" + log.warn "sendPushAndFeeds >> atomicState.timeSendPush: ${atomicState.timeSendPush}" + if (atomicState.timeSendPush){ + if (now() - atomicState.timeSendPush > 86400000){ // notification is sent to remind user once a day + sendPush("Your Ecobee thermostat " + notificationMessage) + sendActivityFeeds(notificationMessage) + atomicState.timeSendPush = now() + } + } else { + sendPush("Your Ecobee thermostat " + notificationMessage) + sendActivityFeeds(notificationMessage) + atomicState.timeSendPush = now() + } + atomicState.authToken = null +} + +def sendActivityFeeds(notificationMessage) { + def devices = getChildDevices() + devices.each { child -> + child.generateActivityFeedsEvent(notificationMessage) //parse received message from parent + } +} From e9996b9fd70ce0e7a043dffe66bf775cc671fab2 Mon Sep 17 00:00:00 2001 From: Warodom Khamphanchai Date: Fri, 4 Dec 2015 16:13:01 -0800 Subject: [PATCH 5/6] Ecobee: round temperature value, add ability to set temperature setpoint temporarily or permanently --- .../ecobee-thermostat.groovy | 26 ++++++++----------- .../ecobee-connect.src/ecobee-connect.groovy | 15 ++++++----- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy index 560adbf..dac3df7 100644 --- a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy +++ b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy @@ -98,19 +98,12 @@ metadata { details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh"]) } + preferences { + input "holdType", "enum", title: "Hold Type", description: "When changing temperature, use Temporary or Permanent hold", required: false, options:["Temporary", "Permanent"] + } + } -/* - - 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"] - } - -*/ - - // parse events into attributes def parse(String description) { log.debug "Parsing '${description}'" @@ -208,7 +201,8 @@ void setHeatingSetpoint(Double setpoint) { log.debug "Sending setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}" - if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId)) { + def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite" + if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId, sendHoldType)) { sendEvent("name":"heatingSetpoint", "value":heatingSetpoint) sendEvent("name":"coolingSetpoint", "value":coolingSetpoint) log.debug "Done setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}" @@ -242,7 +236,8 @@ void setCoolingSetpoint(Double setpoint) { log.debug "Sending setCoolingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}" - if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId)) { + def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite" + if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId, sendHoldType)) { sendEvent("name":"heatingSetpoint", "value":heatingSetpoint) sendEvent("name":"coolingSetpoint", "value":coolingSetpoint) log.debug "Done setCoolingSetpoint>> coolingSetpoint = ${coolingSetpoint}, heatingSetpoint = ${heatingSetpoint}" @@ -608,10 +603,11 @@ void alterSetpoint(temp) { } log.debug "alterSetpoint >> in mode ${mode} trying to change heatingSetpoint to ${targetHeatingSetpoint} " + - "coolingSetpoint to ${targetCoolingSetpoint}" + "coolingSetpoint to ${targetCoolingSetpoint} with holdType : ${holdType}" + def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite" //step2: call parent.setHold to send http request to 3rd party cloud - if (parent.setHold(this, targetHeatingSetpoint, targetCoolingSetpoint, deviceId)) { + if (parent.setHold(this, targetHeatingSetpoint, targetCoolingSetpoint, deviceId, sendHoldType)) { sendEvent("name": "thermostatSetpoint", "value": temp.value.toString(), displayed: false) sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint) sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint) diff --git a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy index 790820d..3ff0ee6 100644 --- a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy +++ b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy @@ -404,7 +404,7 @@ def pollChildren(child = null) { // } if(resp.status == 200) { - log.debug "poll results returned" + log.debug "poll results returned resp.data ${resp.data}" atomicState.remoteSensors = resp.data.thermostatList.remoteSensors atomicState.thermostatData = resp.data updateSensorData() @@ -423,7 +423,9 @@ def pollChildren(child = null) { coolingSetpoint: stat.runtime.desiredCool / 10, thermostatMode: stat.settings.hvacMode ] - data["temperature"] = data["temperature"] ? String.format("%.1f", data["temperature"]) : data["temperature"] + data["temperature"] = data["temperature"] ? data["temperature"].toDouble().toInteger() : data["temperature"] + data["heatingSetpoint"] = data["heatingSetpoint"] ? data["heatingSetpoint"].toDouble().toInteger() : data["heatingSetpoint"] + data["coolingSetpoint"] = data["coolingSetpoint"] ? data["coolingSetpoint"].toDouble().toInteger() : data["coolingSetpoint"] // debugEventFromParent(child, "Event Data = ${data}") collector[dni] = [data:data] @@ -547,9 +549,8 @@ def updateSensorData() { it.capability.each { if (it.type == "temperature") { temperature = it.value as Double - temperature = temperature / 10 - } - if (it.type == "occupancy") { + temperature = (temperature / 10).toInteger() + } else if (it.type == "occupancy") { if(it.value == "true") occupancy = "active" else @@ -658,7 +659,7 @@ def resumeProgram(child, deviceId) { return result } -def setHold(child, heating, cooling, deviceId) { +def setHold(child, heating, cooling, deviceId, sendHoldType) { int h = heating * 10 int c = cooling * 10 @@ -666,7 +667,7 @@ def setHold(child, heating, cooling, deviceId) { // 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":"' + deviceId + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": '+sendHoldType+' } } ]}' // def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}, { "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": "indefinite" } } ]}' def result = sendJson(child, jsonRequestBody) // debugEventFromParent(child, "setHold: heating: ${h}, cooling: ${c} with result ${result}") From e1c52454c64832d8e022a3c06be4d860084e7e01 Mon Sep 17 00:00:00 2001 From: bflorian Date: Sat, 5 Dec 2015 10:21:29 -0500 Subject: [PATCH 6/6] SSQA-65, add text about international phone numbers --- smartapps/smartthings/notify-me-when.src/notify-me-when.groovy | 1 + 1 file changed, 1 insertion(+) diff --git a/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy b/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy index e879ee4..12fa2a7 100644 --- a/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy +++ b/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy @@ -49,6 +49,7 @@ preferences { section("Via a push notification and/or an SMS message"){ input("recipients", "contact", title: "Send notifications to") { input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false + paragraph "If outside the US please make sure to enter the proper country code" input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes", "No"] } }