From e545842f7cf5d867d90318bc8141d28ef4d0b4c7 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Tue, 23 Aug 2016 23:27:30 -0500 Subject: [PATCH 01/18] [DVCSMP-1980] some cleanup of Ecobee Connect SmartApp --- .../ecobee-thermostat.groovy | 12 +- .../ecobee-connect.src/ecobee-connect.groovy | 637 ++++++++++-------- 2 files changed, 367 insertions(+), 282 deletions(-) diff --git a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy index 7b08b14..5021089 100644 --- a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy +++ b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy @@ -234,7 +234,7 @@ void setHeatingSetpoint(setpoint) { def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite" - if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) { + if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) { sendEvent("name":"heatingSetpoint", "value":heatingSetpoint) sendEvent("name":"coolingSetpoint", "value":coolingSetpoint) log.debug "Done setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}" @@ -271,7 +271,7 @@ void setCoolingSetpoint(setpoint) { def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite" - if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) { + if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) { sendEvent("name":"heatingSetpoint", "value":heatingSetpoint) sendEvent("name":"coolingSetpoint", "value":coolingSetpoint) log.debug "Done setCoolingSetpoint>> coolingSetpoint = ${coolingSetpoint}, heatingSetpoint = ${heatingSetpoint}" @@ -287,14 +287,14 @@ void resumeProgram() { log.debug "resumeProgram() is called" sendEvent("name":"thermostatStatus", "value":"resuming schedule", "description":statusText, displayed: false) def deviceId = device.deviceNetworkId.split(/\./).last() - if (parent.resumeProgram(this, deviceId)) { + if (parent.resumeProgram(deviceId)) { sendEvent("name":"thermostatStatus", "value":"setpoint is updating", "description":statusText, displayed: false) runIn(5, "poll") log.debug "resumeProgram() is done" sendEvent("name":"resumeProgram", "value":"resume", descriptionText: "resumeProgram is done", displayed: false, isStateChange: true) } else { sendEvent("name":"thermostatStatus", "value":"failed resume click refresh", "description":statusText, displayed: false) - log.error "Error resumeProgram() check parent.resumeProgram(this, deviceId)" + log.error "Error resumeProgram() check parent.resumeProgram(deviceId)" } } @@ -510,7 +510,7 @@ def fanAuto() { def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint - if (parent.setFanMode(this, heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) { + if (parent.setFanMode(heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) { generateFanModeEvent(fanMode) } else { log.debug "Error setting new mode." @@ -690,7 +690,7 @@ void alterSetpoint(temp) { def coolingValue = location.temperatureScale == "C"? convertCtoF(targetCoolingSetpoint) : targetCoolingSetpoint def heatingValue = location.temperatureScale == "C"? convertCtoF(targetHeatingSetpoint) : targetHeatingSetpoint - if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) { + if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) { sendEvent("name": "thermostatSetpoint", "value": temp.value, displayed: false) sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint) sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint) diff --git a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy index 848b7b7..c50651e 100644 --- a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy +++ b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy @@ -66,7 +66,7 @@ def authPage() { // get rid of next button until the user is actually auth'd if (!oauthTokenProvided) { return dynamicPage(name: "auth", title: "Login", nextPage: "", uninstall:uninstallAllowed) { - section(){ + section() { paragraph "Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button." href url:redirectUrl, style:"embedded", required:true, title:"ecobee", description:description } @@ -76,7 +76,7 @@ def authPage() { log.debug "thermostat list: $stats" log.debug "sensor list: ${sensorsDiscovered()}" return dynamicPage(name: "auth", title: "Select Your Thermostats", uninstall: true) { - section(""){ + section("") { paragraph "Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings." input(name: "thermostats", title:"", type: "enum", required:true, multiple:true, description: "Tap to choose", metadata:[values:stats]) } @@ -84,7 +84,7 @@ def authPage() { def options = sensorsDiscovered() ?: [] def numFound = options.size() ?: 0 if (numFound > 0) { - section(""){ + section("") { paragraph "Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings." input(name: "ecobeesensors", title:"Select Ecobee Sensors (${numFound} found)", type: "enum", required:false, description: "Tap to choose", multiple:true, options:options) } @@ -115,13 +115,12 @@ def callback() { def code = params.code def oauthState = params.state - if (oauthState == atomicState.oauthInitState){ - + if (oauthState == atomicState.oauthInitState) { def tokenParams = [ - grant_type: "authorization_code", - code : code, - client_id : smartThingsClientId, - redirect_uri: callbackUrl + grant_type: "authorization_code", + code : code, + client_id : smartThingsClientId, + redirect_uri: callbackUrl ] def tokenUrl = "https://www.ecobee.com/home/token?${toQueryString(tokenParams)}" @@ -148,8 +147,8 @@ def callback() { def success() { def message = """ -

Your ecobee Account is now connected to SmartThings!

-

Click 'Done' to finish setup.

+

Your ecobee Account is now connected to SmartThings!

+

Click 'Done' to finish setup.

""" connectionStatus(message) } @@ -171,64 +170,63 @@ def connectionStatus(message, redirectUrl = null) { } def html = """ - - - - -Ecobee & SmartThings connection - - - -
+ + + + + Ecobee & SmartThings connection + + + +
ecobee icon connected device icon SmartThings logo ${message} -
- - -""" +
+ + + """ render contentType: 'text/html', data: html } @@ -237,19 +235,26 @@ def getEcobeeThermostats() { log.debug "getting device list" atomicState.remoteSensors = [] - def requestBody = '{"selection":{"selectionType":"registered","selectionMatch":"","includeRuntime":true,"includeSensors":true}}' - + def bodyParams = [ + selection: [ + selectionType: "registered", + selectionMatch: "", + includeRuntime: true, + includeSensors: true + ] + ] def deviceListParams = [ - uri: apiEndpoint, - path: "/1/thermostat", - headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], - query: [format: 'json', body: requestBody] + uri: apiEndpoint, + path: "/1/thermostat", + headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], + // TODO - the query string below is not consistent with the Ecobee docs: + // https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml + query: [format: 'json', body: toJson(bodyParams)] ] def stats = [:] try { httpGet(deviceListParams) { resp -> - if (resp.status == 200) { resp.data.thermostatList.each { stat -> atomicState.remoteSensors = atomicState.remoteSensors == null ? stat.remoteSensors : atomicState.remoteSensors << stat.remoteSensors @@ -289,8 +294,9 @@ Map sensorsDiscovered() { } def getThermostatDisplayName(stat) { - if(stat?.name) - return stat.name.toString() + if(stat?.name) { + return stat.name.toString() + } return (getThermostatTypeName(stat) + " (${stat.identifier})").toString() } @@ -310,7 +316,6 @@ def updated() { } def initialize() { - log.debug "initialize" def devices = thermostats.collect { dni -> def d = getChildDevice(dni) @@ -350,8 +355,6 @@ def initialize() { log.warn "delete: ${delete}, deleting ${delete.size()} thermostats" delete.each { deleteChildDevice(it.deviceNetworkId) } //inherits from SmartApp (data-management) - atomicState.thermostatData = [:] //reset Map to store thermostat data - //send activity feeds to tell that device is connected def notificationMessage = "is connected to SmartThings" sendActivityFeeds(notificationMessage) @@ -383,16 +386,27 @@ def pollHandler() { def pollChildren(child = null) { def thermostatIdsString = getChildDeviceIdsString() log.debug "polling children: $thermostatIdsString" - def data = "" - def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeExtendedRuntime":"true","includeSettings":"true","includeRuntime":"true","includeSensors":true}}' + def requestBody = [ + selection: [ + selectionType: "thermostats", + selectionMatch: thermostatIdsString, + includeExtendedRuntime: true, + includeSettings: true, + includeRuntime: true, + includeSensors: true + ] + ] + def result = false def pollParams = [ - uri: apiEndpoint, - path: "/1/thermostat", - headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], - query: [format: 'json', body: jsonRequestBody] + uri: apiEndpoint, + path: "/1/thermostat", + headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], + // TODO - the query string below is not consistent with the Ecobee docs: + // https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml + query: [format: 'json', body: toJson(requestBody)] ] try{ @@ -400,53 +414,8 @@ def pollChildren(child = null) { if(resp.status == 200) { log.debug "poll results returned resp.data ${resp.data}" atomicState.remoteSensors = resp.data.thermostatList.remoteSensors - atomicState.thermostatData = resp.data updateSensorData() - atomicState.thermostats = resp.data.thermostatList.inject([:]) { collector, stat -> - def dni = [ app.id, stat.identifier ].join('.') - - log.debug "updating dni $dni" - - data = [ - coolMode: (stat.settings.coolStages > 0), - heatMode: (stat.settings.heatStages > 0), - deviceTemperatureUnit: stat.settings.useCelsius, - minHeatingSetpoint: (stat.settings.heatRangeLow / 10), - maxHeatingSetpoint: (stat.settings.heatRangeHigh / 10), - minCoolingSetpoint: (stat.settings.coolRangeLow / 10), - maxCoolingSetpoint: (stat.settings.coolRangeHigh / 10), - autoMode: stat.settings.autoHeatCoolFeatureEnabled, - auxHeatMode: (stat.settings.hasHeatPump) && (stat.settings.hasForcedAir || stat.settings.hasElectric || stat.settings.hasBoiler), - temperature: (stat.runtime.actualTemperature / 10), - heatingSetpoint: stat.runtime.desiredHeat / 10, - coolingSetpoint: stat.runtime.desiredCool / 10, - thermostatMode: stat.settings.hvacMode, - humidity: stat.runtime.actualHumidity, - thermostatFanMode: stat.runtime.desiredFanMode - ] - - if (location.temperatureScale == "F") - { - data["temperature"] = data["temperature"] ? Math.round(data["temperature"].toDouble()) : data["temperature"] - data["heatingSetpoint"] = data["heatingSetpoint"] ? Math.round(data["heatingSetpoint"].toDouble()) : data["heatingSetpoint"] - data["coolingSetpoint"] = data["coolingSetpoint"] ? Math.round(data["coolingSetpoint"].toDouble()) : data["coolingSetpoint"] - data["minHeatingSetpoint"] = data["minHeatingSetpoint"] ? Math.round(data["minHeatingSetpoint"].toDouble()) : data["minHeatingSetpoint"] - data["maxHeatingSetpoint"] = data["maxHeatingSetpoint"] ? Math.round(data["maxHeatingSetpoint"].toDouble()) : data["maxHeatingSetpoint"] - data["minCoolingSetpoint"] = data["minCoolingSetpoint"] ? Math.round(data["minCoolingSetpoint"].toDouble()) : data["minCoolingSetpoint"] - data["maxCoolingSetpoint"] = data["maxCoolingSetpoint"] ? Math.round(data["maxCoolingSetpoint"].toDouble()) : data["maxCoolingSetpoint"] - - } - - if (data?.deviceTemperatureUnit == false && location.temperatureScale == "F") { - data["deviceTemperatureUnit"] = "F" - - } else { - data["deviceTemperatureUnit"] = "C" - } - - collector[dni] = [data:data] - return collector - } + storeThermostatData(resp.data.thermostatList) result = true log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}" } @@ -463,13 +432,12 @@ def pollChildren(child = null) { } // Poll Child is invoked from the Child Device itself as part of the Poll Capability -def pollChild(){ - +def pollChild() { def devices = getChildDevices() - if (pollChildren()){ + if (pollChildren()) { devices.each { child -> - if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")){ + if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")) { if(atomicState.thermostats[child.device.deviceNetworkId] != null) { def tData = atomicState.thermostats[child.device.deviceNetworkId] log.info "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}" @@ -492,36 +460,7 @@ void poll() { } def availableModes(child) { - debugEvent ("atomicState.thermostats = ${atomicState.thermostats}") - - debugEvent ("Child DNI = ${child.device.deviceNetworkId}") - - def tData = atomicState.thermostats[child.device.deviceNetworkId] - - debugEvent("Data = ${tData}") - - if(!tData) - { - log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" - - return null - } - - def modes = ["off"] - - if (tData.data.heatMode) modes.add("heat") - if (tData.data.coolMode) modes.add("cool") - if (tData.data.autoMode) modes.add("auto") - if (tData.data.auxHeatMode) modes.add("auxHeatOnly") - - modes - -} - -def currentMode(child) { - debugEvent ("atomicState.Thermos = ${atomicState.thermostats}") - debugEvent ("Child DNI = ${child.device.deviceNetworkId}") def tData = atomicState.thermostats[child.device.deviceNetworkId] @@ -530,14 +469,42 @@ def currentMode(child) { if(!tData) { log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" + return null + } + def modes = ["off"] + if (tData.data.heatMode) { + modes.add("heat") + } + if (tData.data.coolMode) { + modes.add("cool") + } + if (tData.data.autoMode) { + modes.add("auto") + } + if (tData.data.auxHeatMode) { + modes.add("auxHeatOnly") + } + + return modes +} + +def currentMode(child) { + debugEvent ("atomicState.Thermos = ${atomicState.thermostats}") + debugEvent ("Child DNI = ${child.device.deviceNetworkId}") + + def tData = atomicState.thermostats[child.device.deviceNetworkId] + + debugEvent("Data = ${tData}") + + if(!tData) { + log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" return null } def mode = tData.data.thermostatMode - - mode + return mode } def updateSensorData() { @@ -558,12 +525,12 @@ def updateSensorData() { } } - } else if (it.type == "occupancy") { - if(it.value == "true") - occupancy = "active" - else + if(it.value == "true") { + occupancy = "active" + } else { occupancy = "inactive" + } } } def dni = "ecobee_sensor-"+ it?.id + "-" + it?.code @@ -582,7 +549,7 @@ def getChildDeviceIdsString() { } def toJson(Map m) { - return new org.json.JSONObject(m).toString() + return groovy.json.JsonOutput.toJson(m) } def toQueryString(Map m) { @@ -595,54 +562,26 @@ private refreshAuthToken() { if(!atomicState.refreshToken) { log.warn "Can not refresh OAuth token since there is no refreshToken stored" } else { - def refreshParams = [ - method: 'POST', - uri : apiEndpoint, - path : "/token", - query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId], + method: 'POST', + uri : apiEndpoint, + path : "/token", + query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId], ] - log.debug refreshParams + log.debug "making refresh request with paramaters: $refreshParams" def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials." //changed to httpPost try { def jsonMap httpPost(refreshParams) { resp -> - if(resp.status == 200) { log.debug "Token refreshed...calling saved RestAction now!" - debugEvent("Token refreshed ... calling saved RestAction now!") - - log.debug resp - - jsonMap = resp.data - - if(resp.data) { - - log.debug resp.data - debugEvent("Response = ${resp.data}") - - atomicState.refreshToken = resp?.data?.refresh_token - atomicState.authToken = resp?.data?.access_token - - debugEvent("Refresh Token = ${atomicState.refreshToken}") - debugEvent("OAUTH Token = ${atomicState.authToken}") - - if(atomicState.action && atomicState.action != "") { - log.debug "Executing next action: ${atomicState.action}" - - "${atomicState.action}"() - - atomicState.action = "" - } - - } - atomicState.action = "" - } - } + saveTokenAndResumeAction(resp.data) + } + } } catch (groovyx.net.http.HttpResponseException e) { log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}" def reAttemptPeriod = 300 // in sec @@ -662,64 +601,174 @@ private refreshAuthToken() { } } -def resumeProgram(child, deviceId) { - - - def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}]}' - def result = sendJson(jsonRequestBody) - return result +/** + * Saves the refresh and auth token from the passed-in JSON object, + * and invokes any previously executing action that did not complete due to + * an expired token. + * + * @param json - an object representing the parsed JSON response from Ecobee + */ +private void saveTokenAndResumeAction(json) { + log.debug "token response json: $json" + if (json) { + debugEvent("Response = $json") + atomicState.refreshToken = json?.refresh_token + atomicState.authToken = json?.access_token + if (atomicState.action) { + log.debug "ASYNC refreshTokenHandler: got refresh token, executing next action: ${atomicState.action}" + "${atomicState.action}"() + } + } else { + log.warn "did not get response body from refresh token response" + } + atomicState.action = "" } -def setHold(child, heating, cooling, deviceId, sendHoldType) { - - int h = heating * 10 - int c = cooling * 10 - def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": '+sendHoldType+' } } ]}' - - def result = sendJson(child, jsonRequestBody) - return result +/** + * Executes the resume program command on the Ecobee thermostat + * @param deviceId - the ID of the device + * + * @retrun true if the command was successful, false otherwise. + */ +boolean resumeProgram(deviceId) { + def payload = [ + selection: [ + selectionType: "thermostats", + selectionMatch: deviceId, + includeRuntime: true + ], + functions: [ + [ + type: "resumeProgram" + ] + ] + ] + return sendCommandToEcobee(payload) } -def setFanMode(child, heating, cooling, deviceId, sendHoldType, fanMode) { +/** + * Executes the set hold command on the Ecobee thermostat + * @param heating - The heating temperature to set in fahrenheit + * @param cooling - the cooling temperature to set in fahrenheit + * @param deviceId - the ID of the device + * @param sendHoldType - the hold type to execute + * + * @return true if the command was successful, false otherwise + */ +boolean setHold(heating, cooling, deviceId, sendHoldType) { + // Ecobee requires that temp values be in fahrenheit multiplied by 10. + int h = heating * 10 + int c = cooling * 10 - int h = heating * 10 - int c = cooling * 10 + def payload = [ + selection: [ + selectionType: "thermostats", + selectionMatch: deviceId, + includeRuntime: true + ], + functions: [ + [ + type: "setHold", + params: [ + coolHoldTemp: c, + heatHoldTemp: h, + holdType: sendHoldType + ] + ] + ] + ] - - def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": '+sendHoldType+', "fan": '+fanMode+' } } ]}' - def result = sendJson(child, jsonRequestBody) - return result + return sendCommandToEcobee(payload) } -def setMode(child, mode, deviceId) { - def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"thermostat": {"settings":{"hvacMode":"'+"${mode}"+'"}}}' +/** + * Executes the set fan mode command on the Ecobee thermostat + * @param heating - The heating temperature to set in fahrenheit + * @param cooling - the cooling temperature to set in fahrenheit + * @param deviceId - the ID of the device + * @param sendHoldType - the hold type to execute + * @param fanMode - the fan mode to set to + * + * @return true if the command was successful, false otherwise + */ +boolean setFanMode(heating, cooling, deviceId, sendHoldType, fanMode) { + // Ecobee requires that temp values be in fahrenheit multiplied by 10. + int h = heating * 10 + int c = cooling * 10 - def result = sendJson(jsonRequestBody) - return result + def payload = [ + selection: [ + selectionType: "thermostats", + selectionMatch: deviceId, + includeRuntime: true + ], + functions: [ + [ + type: "setHold", + params: [ + coolHoldTemp: c, + heatHoldTemp: h, + holdType: sendHoldType, + fan: fanMode + ] + ] + ] + ] + + return sendCommandToEcobee(payload) } -def sendJson(child = null, String jsonBody) { +/** + * Sets the mode of the Ecobee thermostat + * @param mode - the mode to set to + * @param deviceId - the ID of the device + * + * @return true if the command was successful, false otherwise + */ +boolean setMode(mode, deviceId) { + def payload = [ + selection: [ + selectionType: "thermostats", + selectionMatch: deviceId, + includeRuntime: true + ], + thermostat: [ + settings: [ + hvacMode: mode + ] + ] + ] + return sendCommandToEcobee(payload) +} - def returnStatus = false +/** + * Makes a request to the Ecobee API to actuate the thermostat. + * Used by command methods to send commands to Ecobee. + * + * @param bodyParams - a map of request parameters to send to Ecobee. + * + * @return true if the command was accepted by Ecobee without error, false otherwise. + */ +private boolean sendCommandToEcobee(Map bodyParams) { + def isSuccess = false def cmdParams = [ - uri: apiEndpoint, - path: "/1/thermostat", - headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"], - body: jsonBody + uri: apiEndpoint, + path: "/1/thermostat", + headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"], + body: toJson(bodyParams) ] try{ httpPost(cmdParams) { resp -> - if(resp.status == 200) { - log.debug "updated ${resp.data}" - returnStatus = resp.data.status.code - if (resp.data.status.code == 0) - log.debug "Successful call to ecobee API." - else { - log.debug "Error return code = ${resp.data.status.code}" - debugEvent("Error return code = ${resp.data.status.code}") + def returnStatus = resp.data.status.code + if (returnStatus == 0) { + log.debug "Successful call to ecobee API." + isSuccess = true + } else { + log.debug "Error return code = ${returnStatus}" + debugEvent("Error return code = ${returnStatus}") } } } @@ -727,53 +776,45 @@ def sendJson(child = null, String jsonBody) { log.trace "Exception Sending Json: " + e.response.data.status debugEvent ("sent Json & got http status ${e.statusCode} - ${e.response.data.status.code}") if (e.response.data.status.code == 14) { + // TODO - figure out why we're setting the next action to be pollChildren + // after refreshing auth token. Is it to keep UI in sync, or just copy/paste error? atomicState.action = "pollChildren" log.debug "Refreshing your auth_token!" refreshAuthToken() - } - else { + } else { debugEvent("Authentication error, invalid authentication method, lack of credentials, etc.") log.error "Authentication error, invalid authentication method, lack of credentials, etc." } } - if (returnStatus == 0) - return true - else - return false + return isSuccess } -def getChildName() { "Ecobee Thermostat" } -def getSensorChildName() { "Ecobee Sensor" } +def getChildName() { return "Ecobee Thermostat" } +def getSensorChildName() { return "Ecobee Sensor" } def getServerUrl() { return "https://graph.api.smartthings.com" } def getShardUrl() { return getApiServerUrl() } -def getCallbackUrl() { "https://graph.api.smartthings.com/oauth/callback" } -def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${shardUrl}" } -def getApiEndpoint() { "https://api.ecobee.com" } -def getSmartThingsClientId() { appSettings.clientId } +def getCallbackUrl() { return "https://graph.api.smartthings.com/oauth/callback" } +def getBuildRedirectUrl() { return "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${shardUrl}" } +def getApiEndpoint() { return "https://api.ecobee.com" } +def getSmartThingsClientId() { return appSettings.clientId } def debugEvent(message, displayEvent = false) { - def results = [ - name: "appdebug", - descriptionText: message, - displayed: displayEvent + name: "appdebug", + descriptionText: message, + displayed: displayEvent ] log.debug "Generating AppDebug Event: ${results}" sendEvent (results) - -} - -def debugEventFromParent(child, message) { - if (child != null) { child.sendEvent("name":"debugEventFromParent", "value":message, "description":message, displayed: true, isStateChange: true)} } //send both push notification and mobile activity feeds -def sendPushAndFeeds(notificationMessage){ +def sendPushAndFeeds(notificationMessage) { log.warn "sendPushAndFeeds >> notificationMessage: ${notificationMessage}" log.warn "sendPushAndFeeds >> atomicState.timeSendPush: ${atomicState.timeSendPush}" - if (atomicState.timeSendPush){ - if (now() - atomicState.timeSendPush > 86400000){ // notification is sent to remind user once a day + if (atomicState.timeSendPush) { + if (now() - atomicState.timeSendPush > 86400000) { // notification is sent to remind user once a day sendPush("Your Ecobee thermostat " + notificationMessage) sendActivityFeeds(notificationMessage) atomicState.timeSendPush = now() @@ -786,6 +827,58 @@ def sendPushAndFeeds(notificationMessage){ atomicState.authToken = null } +/** + * Stores data about the thermostats in atomicState. + * @param thermostats - a list of thermostats as returned from the Ecobee API + */ +private void storeThermostatData(thermostats) { + log.trace "Storing thermostat data: $thermostats" + def data + atomicState.thermostats = thermostats.inject([:]) { collector, stat -> + def dni = [ app.id, stat.identifier ].join('.') + log.debug "updating dni $dni" + + data = [ + coolMode: (stat.settings.coolStages > 0), + heatMode: (stat.settings.heatStages > 0), + deviceTemperatureUnit: stat.settings.useCelsius, + minHeatingSetpoint: (stat.settings.heatRangeLow / 10), + maxHeatingSetpoint: (stat.settings.heatRangeHigh / 10), + minCoolingSetpoint: (stat.settings.coolRangeLow / 10), + maxCoolingSetpoint: (stat.settings.coolRangeHigh / 10), + autoMode: stat.settings.autoHeatCoolFeatureEnabled, + auxHeatMode: (stat.settings.hasHeatPump) && (stat.settings.hasForcedAir || stat.settings.hasElectric || stat.settings.hasBoiler), + temperature: (stat.runtime.actualTemperature / 10), + heatingSetpoint: stat.runtime.desiredHeat / 10, + coolingSetpoint: stat.runtime.desiredCool / 10, + thermostatMode: stat.settings.hvacMode, + humidity: stat.runtime.actualHumidity, + thermostatFanMode: stat.runtime.desiredFanMode + ] + if (location.temperatureScale == "F") { + data["temperature"] = data["temperature"] ? Math.round(data["temperature"].toDouble()) : data["temperature"] + data["heatingSetpoint"] = data["heatingSetpoint"] ? Math.round(data["heatingSetpoint"].toDouble()) : data["heatingSetpoint"] + data["coolingSetpoint"] = data["coolingSetpoint"] ? Math.round(data["coolingSetpoint"].toDouble()) : data["coolingSetpoint"] + data["minHeatingSetpoint"] = data["minHeatingSetpoint"] ? Math.round(data["minHeatingSetpoint"].toDouble()) : data["minHeatingSetpoint"] + data["maxHeatingSetpoint"] = data["maxHeatingSetpoint"] ? Math.round(data["maxHeatingSetpoint"].toDouble()) : data["maxHeatingSetpoint"] + data["minCoolingSetpoint"] = data["minCoolingSetpoint"] ? Math.round(data["minCoolingSetpoint"].toDouble()) : data["minCoolingSetpoint"] + data["maxCoolingSetpoint"] = data["maxCoolingSetpoint"] ? Math.round(data["maxCoolingSetpoint"].toDouble()) : data["maxCoolingSetpoint"] + + } + + if (data?.deviceTemperatureUnit == false && location.temperatureScale == "F") { + data["deviceTemperatureUnit"] = "F" + + } else { + data["deviceTemperatureUnit"] = "C" + } + + collector[dni] = [data:data] + return collector + } + log.debug "updated ${atomicState.thermostats?.size()} thermostats: ${atomicState.thermostats}" +} + def sendActivityFeeds(notificationMessage) { def devices = getChildDevices() devices.each { child -> @@ -793,14 +886,6 @@ def sendActivityFeeds(notificationMessage) { } } -def roundC (tempC) { - return String.format("%.1f", (Math.round(tempC * 2))/2) -} - def convertFtoC (tempF) { return String.format("%.1f", (Math.round(((tempF - 32)*(5/9)) * 2))/2) } - -def convertCtoF (tempC) { - return (Math.round(tempC * (9/5)) + 32).toInteger() -} From 92cc8afdf711c00a74d21943d091abb30e469a87 Mon Sep 17 00:00:00 2001 From: ShunmugaSundar Date: Tue, 23 Aug 2016 17:49:58 +0530 Subject: [PATCH 02/18] CHF-240 : Adding readme.md files for below devices 1. SmartSense Moisture Sensor 2. SmartSense Temp/Humidity Sensor 3. SmartSense Multi Sensor 4. SmartSense Open/closed Sensor 5. SmartPower Outlet 6. Connected Cree Bulb 7. Aeon Multisensor 8. Aeon Multisensor Gen5 9. Aeon Multisensor 6 10. SmartSense Motion Sensor CHF-240 : Added readme.md files for below devices 1. SmartSense Moisture Sensor 2. SmartSense Temp/Humidity Sensor 3. SmartSense Multi Sensor 4. SmartSense Open/closed Sensor 5. SmartPower Outlet 6. Connected Cree Bulb 7. SmartSense Motion Sensor --- .../smartthings/cree-bulb.src/.st-ignore | 2 ++ .../smartthings/cree-bulb.src/README.md | 29 +++++++++++++++ .../smartpower-outlet.src/.st-ignore | 2 ++ .../smartpower-outlet.src/README.md | 29 +++++++++++++++ .../smartsense-moisture-sensor.src/.st-ignore | 2 ++ .../smartsense-moisture-sensor.src/README.md | 33 +++++++++++++++++ .../smartsense-motion-sensor.src/README.md | 14 ++++---- .../smartsense-multi-sensor.src/.st-ignore | 2 ++ .../smartsense-multi-sensor.src/README.md | 36 +++++++++++++++++++ .../.st-ignore | 2 ++ .../README.md | 34 ++++++++++++++++++ .../.st-ignore | 2 ++ .../README.md | 34 ++++++++++++++++++ 13 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 devicetypes/smartthings/cree-bulb.src/.st-ignore create mode 100644 devicetypes/smartthings/cree-bulb.src/README.md create mode 100644 devicetypes/smartthings/smartpower-outlet.src/.st-ignore create mode 100644 devicetypes/smartthings/smartpower-outlet.src/README.md create mode 100644 devicetypes/smartthings/smartsense-moisture-sensor.src/.st-ignore create mode 100644 devicetypes/smartthings/smartsense-moisture-sensor.src/README.md create mode 100644 devicetypes/smartthings/smartsense-multi-sensor.src/.st-ignore create mode 100644 devicetypes/smartthings/smartsense-multi-sensor.src/README.md create mode 100644 devicetypes/smartthings/smartsense-open-closed-sensor.src/.st-ignore create mode 100644 devicetypes/smartthings/smartsense-open-closed-sensor.src/README.md create mode 100644 devicetypes/smartthings/smartsense-temp-humidity-sensor.src/.st-ignore create mode 100644 devicetypes/smartthings/smartsense-temp-humidity-sensor.src/README.md diff --git a/devicetypes/smartthings/cree-bulb.src/.st-ignore b/devicetypes/smartthings/cree-bulb.src/.st-ignore new file mode 100644 index 0000000..f78b46e --- /dev/null +++ b/devicetypes/smartthings/cree-bulb.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/cree-bulb.src/README.md b/devicetypes/smartthings/cree-bulb.src/README.md new file mode 100644 index 0000000..bde78b1 --- /dev/null +++ b/devicetypes/smartthings/cree-bulb.src/README.md @@ -0,0 +1,29 @@ +# Connected Cree LED Bulb + + + +Works with: + +* [Samsung Connected Cree LED Bulb](https://support.smartthings.com/hc/en-us/articles/204258280-Cree-Connected-LED-Bulb) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) + +## Capabilities + +* **Actuator** - represents that a Device has commands +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Polling** - represents that poll() can be implemented for the device +* **Refresh** - _refresh()_ command for status updates +* **Switch** - can detect state (possible values: on/off) +* **Switch Level** - represents current light level, usually 0-100 in percent +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +A Category C6 Connected Cree LED Bulb with maxReportTime of 10 min. +Check-in interval is double the value of maxReportTime for Zigbee device. +This gives the device twice the amount of time to respond before it is marked as offline. +Check-in interval = 2*10 = 20 min \ No newline at end of file diff --git a/devicetypes/smartthings/smartpower-outlet.src/.st-ignore b/devicetypes/smartthings/smartpower-outlet.src/.st-ignore new file mode 100644 index 0000000..f78b46e --- /dev/null +++ b/devicetypes/smartthings/smartpower-outlet.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/smartpower-outlet.src/README.md b/devicetypes/smartthings/smartpower-outlet.src/README.md new file mode 100644 index 0000000..0a7739a --- /dev/null +++ b/devicetypes/smartthings/smartpower-outlet.src/README.md @@ -0,0 +1,29 @@ +# SmartPower Outlet + + + +Works with: + +* [Samsung SmartPower Outlet](https://shop.smartthings.com/#!/products/smartpower-outlet) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) + +## Capabilities + +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Actuator** - represents that a Device has commands +* **Switch** - can detect state (possible values: on/off) +* **Refresh** - _refresh()_ command for status updates +* **Power Meter** - detects power meter for device in either w or kw. +* **Health Check** - indicates ability to get device health notifications +* **Sensor** - detects sensor events + +## Device Health + +A Category C1 smart power outlet with maxReportTime of 10 min. +Check-in interval is double the value of maxReportTime for Zigbee device. +This gives the device twice the amount of time to respond before it is marked as offline. +Check-in interval = 2*10 = 20 min \ No newline at end of file diff --git a/devicetypes/smartthings/smartsense-moisture-sensor.src/.st-ignore b/devicetypes/smartthings/smartsense-moisture-sensor.src/.st-ignore new file mode 100644 index 0000000..f78b46e --- /dev/null +++ b/devicetypes/smartthings/smartsense-moisture-sensor.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/smartsense-moisture-sensor.src/README.md b/devicetypes/smartthings/smartsense-moisture-sensor.src/README.md new file mode 100644 index 0000000..0f1ac00 --- /dev/null +++ b/devicetypes/smartthings/smartsense-moisture-sensor.src/README.md @@ -0,0 +1,33 @@ +# Smartsense Moisture Sensor + + + +Works with: + +* [Samsung SmartThings Moisture Sensor](https://shop.smartthings.com/#!/products/samsung-smartthings-water-leak-sensor) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) + +## Capabilities + +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Battery** - defines device uses a battery +* **Refresh** - _refresh()_ command for status updates +* **Temperature Measurement** - defines device measures current temperature +* **Water Sensor** - can detect presence of water (dry or wet) +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +A Category C2 moisture sensor with maxReportTime of 1 hr. +Check-in interval is double the value of maxReportTime for Zigbee device. +This gives the device twice the amount of time to respond before it is marked as offline. +Check-in interval = 2*60 = 120 min + +## Battery Specification + +One CR2 3V battery required. \ No newline at end of file diff --git a/devicetypes/smartthings/smartsense-motion-sensor.src/README.md b/devicetypes/smartthings/smartsense-motion-sensor.src/README.md index 82bff3f..0c8a9d3 100644 --- a/devicetypes/smartthings/smartsense-motion-sensor.src/README.md +++ b/devicetypes/smartthings/smartsense-motion-sensor.src/README.md @@ -9,7 +9,8 @@ Works with: ## Table of contents * [Capabilities](#capabilities) -* [Health]($health) +* [Health](#device-health) +* [Battery](#battery-specification) ## Capabilities @@ -21,10 +22,11 @@ Works with: ## Device Health -A Category C2 motion sensor that has 120min check-in interval - - - - +A Category C2 motion sensor with maxReportTime of 1 hr. +Check-in interval is double the value of maxReportTime for Zigbee device. +This gives the device twice the amount of time to respond before it is marked as offline. +Check-in interval = 2*60 = 120 min +## Battery Specification +One CR123A 3V battery is required. \ No newline at end of file diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/.st-ignore b/devicetypes/smartthings/smartsense-multi-sensor.src/.st-ignore new file mode 100644 index 0000000..f78b46e --- /dev/null +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/README.md b/devicetypes/smartthings/smartsense-multi-sensor.src/README.md new file mode 100644 index 0000000..ccabf1d --- /dev/null +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/README.md @@ -0,0 +1,36 @@ +# Smartsense Multi Sensor + + + +Works with: + +* [Samsung SmartThings Multi Sensor](https://shop.smartthings.com/#!/products/smartsense-multi) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) + +## Capabilities + +* **Three Axis** - monitors the state of a single axis +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Battery** - defines device uses a battery +* **Sensor** - detects sensor events +* **Contact Sensor** - can detect contact (possible values: open,closed) +* **Acceleration Sensor** - allows for acceleration detection. +* **Refresh** - _refresh()_ command for status updates +* **Temperature Measurement** - defines device measures current temperature +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +A Category C2 multi sensor with maxReportTime of 1 hr. +Check-in interval is double the value of maxReportTime for Zigbee device. +This gives the device twice the amount of time to respond before it is marked as offline. +Check-in interval = 2*60 = 120 min + +## Battery Specification + +Two AAAA battery required. \ No newline at end of file diff --git a/devicetypes/smartthings/smartsense-open-closed-sensor.src/.st-ignore b/devicetypes/smartthings/smartsense-open-closed-sensor.src/.st-ignore new file mode 100644 index 0000000..f78b46e --- /dev/null +++ b/devicetypes/smartthings/smartsense-open-closed-sensor.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/smartsense-open-closed-sensor.src/README.md b/devicetypes/smartthings/smartsense-open-closed-sensor.src/README.md new file mode 100644 index 0000000..dd012e5 --- /dev/null +++ b/devicetypes/smartthings/smartsense-open-closed-sensor.src/README.md @@ -0,0 +1,34 @@ +# Smartsense Open/Closed Sensor + + + +Works with: + +* [Samsung SmartThings Open/Closed Sensor](https://shop.smartthings.com/#!/packs/smartsense-open-closed-sensor/) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) + +## Capabilities + +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Battery** - defines device uses a battery +* **Contact Sensor** - can detect contact (possible values: open,closed) +* **Refresh** - _refresh()_ command for status updates +* **Temperature Measurement** - defines device measures current temperature +* **Health Check** - indicates ability to get device health notifications +* **Sensor** - detects sensor events + +## Device Health + +A Category C2 open/closed sensor with maxReportTime of 1 hr. +Check-in interval is double the value of maxReportTime for Zigbee device. +This gives the device twice the amount of time to respond before it is marked as offline. +Check-in interval = 2*60 = 120 min + +## Battery Specification + +One CR2 3V battery required. \ No newline at end of file diff --git a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/.st-ignore b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/.st-ignore new file mode 100644 index 0000000..f78b46e --- /dev/null +++ b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/README.md b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/README.md new file mode 100644 index 0000000..7b2f0ac --- /dev/null +++ b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/README.md @@ -0,0 +1,34 @@ +# SmartSense Temp/Humidity Sensor + + + +Works with: + +* [Samsung SmartSense Temp/Humidity Sensor](https://shop.smartthings.com/#!/products/smartsense-temp-humidity-sensor) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) + +## Capabilities + +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Battery** - defines device uses a battery +* **Relative Humidity Measurement** - defines device measures relative humidity +* **Refresh** - _refresh()_ command for status updates +* **Temperature Measurement** - defines device measures current temperature +* **Health Check** - indicates ability to get device health notifications +* **Sensor** - detects sensor events + +## Device Health + +A Category C2 SmartSense Temp/Humidity Sensor with maxReportTime of 1 hr. +Check-in interval is double the value of maxReportTime for Zigbee device. +This gives the device twice the amount of time to respond before it is marked as offline. +Check-in interval = 2*60 = 120 min + +## Battery Specification + +One CR2 battery is required. \ No newline at end of file From dbc2a1e45c378953cb0c3f1f1d69c29759f1ab3c Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Wed, 24 Aug 2016 07:29:04 -0500 Subject: [PATCH 03/18] a few more whitespace fixes --- .../ecobee-connect.src/ecobee-connect.groovy | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy index c50651e..5922812 100644 --- a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy +++ b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy @@ -294,10 +294,10 @@ Map sensorsDiscovered() { } def getThermostatDisplayName(stat) { - if(stat?.name) { + if(stat?.name) { return stat.name.toString() } - return (getThermostatTypeName(stat) + " (${stat.identifier})").toString() + return (getThermostatTypeName(stat) + " (${stat.identifier})").toString() } def getThermostatTypeName(stat) { @@ -384,8 +384,8 @@ def pollHandler() { } def pollChildren(child = null) { - def thermostatIdsString = getChildDeviceIdsString() - log.debug "polling children: $thermostatIdsString" + def thermostatIdsString = getChildDeviceIdsString() + log.debug "polling children: $thermostatIdsString" def requestBody = [ selection: [ @@ -401,24 +401,24 @@ def pollChildren(child = null) { def result = false def pollParams = [ - uri: apiEndpoint, - path: "/1/thermostat", - headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], + uri: apiEndpoint, + path: "/1/thermostat", + headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], // TODO - the query string below is not consistent with the Ecobee docs: // https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml - query: [format: 'json', body: toJson(requestBody)] - ] + query: [format: 'json', body: toJson(requestBody)] + ] try{ httpGet(pollParams) { resp -> if(resp.status == 200) { - log.debug "poll results returned resp.data ${resp.data}" - atomicState.remoteSensors = resp.data.thermostatList.remoteSensors - updateSensorData() + log.debug "poll results returned resp.data ${resp.data}" + atomicState.remoteSensors = resp.data.thermostatList.remoteSensors + updateSensorData() storeThermostatData(resp.data.thermostatList) - result = true - log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}" - } + result = true + log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}" + } } } catch (groovyx.net.http.HttpResponseException e) { log.trace "Exception polling children: " + e.response.data.status @@ -474,20 +474,20 @@ def availableModes(child) { def modes = ["off"] - if (tData.data.heatMode) { + if (tData.data.heatMode) { modes.add("heat") } - if (tData.data.coolMode) { + if (tData.data.coolMode) { modes.add("cool") } - if (tData.data.autoMode) { + if (tData.data.autoMode) { modes.add("auto") } - if (tData.data.auxHeatMode) { + if (tData.data.auxHeatMode) { modes.add("auxHeatOnly") } - return modes + return modes } def currentMode(child) { @@ -615,7 +615,7 @@ private void saveTokenAndResumeAction(json) { atomicState.refreshToken = json?.refresh_token atomicState.authToken = json?.access_token if (atomicState.action) { - log.debug "ASYNC refreshTokenHandler: got refresh token, executing next action: ${atomicState.action}" + log.debug "got refresh token, executing next action: ${atomicState.action}" "${atomicState.action}"() } } else { @@ -678,7 +678,7 @@ boolean setHold(heating, cooling, deviceId, sendHoldType) { ] ] - return sendCommandToEcobee(payload) + return sendCommandToEcobee(payload) } /** @@ -759,19 +759,19 @@ private boolean sendCommandToEcobee(Map bodyParams) { ] try{ - httpPost(cmdParams) { resp -> - if(resp.status == 200) { - log.debug "updated ${resp.data}" + httpPost(cmdParams) { resp -> + if(resp.status == 200) { + log.debug "updated ${resp.data}" def returnStatus = resp.data.status.code - if (returnStatus == 0) { + if (returnStatus == 0) { log.debug "Successful call to ecobee API." isSuccess = true } else { - log.debug "Error return code = ${returnStatus}" - debugEvent("Error return code = ${returnStatus}") - } - } - } + log.debug "Error return code = ${returnStatus}" + debugEvent("Error return code = ${returnStatus}") + } + } + } } catch (groovyx.net.http.HttpResponseException e) { log.trace "Exception Sending Json: " + e.response.data.status debugEvent ("sent Json & got http status ${e.statusCode} - ${e.response.data.status.code}") From 7ce7ad86bd6ad3e4dc6849871cfc2f632bd4f334 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Thu, 25 Aug 2016 12:48:36 -0500 Subject: [PATCH 04/18] [DVCSMP-1980] fix some missed parent call method signature changes --- .../ecobee-thermostat.src/ecobee-thermostat.groovy | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy index 5021089..2705f56 100644 --- a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy +++ b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy @@ -406,7 +406,7 @@ def generateOperatingStateEvent(operatingState) { def off() { log.debug "off" def deviceId = device.deviceNetworkId.split(/\./).last() - if (parent.setMode (this,"off", deviceId)) + if (parent.setMode ("off", deviceId)) generateModeEvent("off") else { log.debug "Error setting new mode." @@ -420,7 +420,7 @@ def off() { def heat() { log.debug "heat" def deviceId = device.deviceNetworkId.split(/\./).last() - if (parent.setMode (this,"heat", deviceId)) + if (parent.setMode ("heat", deviceId)) generateModeEvent("heat") else { log.debug "Error setting new mode." @@ -438,7 +438,7 @@ def emergencyHeat() { def auxHeatOnly() { log.debug "auxHeatOnly" def deviceId = device.deviceNetworkId.split(/\./).last() - if (parent.setMode (this,"auxHeatOnly", deviceId)) + if (parent.setMode ("auxHeatOnly", deviceId)) generateModeEvent("auxHeatOnly") else { log.debug "Error setting new mode." @@ -452,7 +452,7 @@ def auxHeatOnly() { def cool() { log.debug "cool" def deviceId = device.deviceNetworkId.split(/\./).last() - if (parent.setMode (this,"cool", deviceId)) + if (parent.setMode ("cool", deviceId)) generateModeEvent("cool") else { log.debug "Error setting new mode." @@ -466,7 +466,7 @@ def cool() { def auto() { log.debug "auto" def deviceId = device.deviceNetworkId.split(/\./).last() - if (parent.setMode (this,"auto", deviceId)) + if (parent.setMode ("auto", deviceId)) generateModeEvent("auto") else { log.debug "Error setting new mode." @@ -489,7 +489,7 @@ def fanOn() { def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint - if (parent.setFanMode(this, heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) { + if (parent.setFanMode(heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) { generateFanModeEvent(fanMode) } else { log.debug "Error setting new mode." From 655e756b1bf7577f9c0226dea0c4a825a4447ecf Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Thu, 25 Aug 2016 20:03:20 -0400 Subject: [PATCH 05/18] DVCSMP-1959 - Remove logs with sensitive information (#1152) --- .../jawbone-button-notifier.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smartapps/juano2310/jawbone-button-notifier.src/jawbone-button-notifier.groovy b/smartapps/juano2310/jawbone-button-notifier.src/jawbone-button-notifier.groovy index 9ea4f3e..41b7211 100644 --- a/smartapps/juano2310/jawbone-button-notifier.src/jawbone-button-notifier.groovy +++ b/smartapps/juano2310/jawbone-button-notifier.src/jawbone-button-notifier.groovy @@ -62,7 +62,7 @@ def initialize() { } def sendit(evt) { - log.debug "$evt.value: $evt, $settings" + log.debug "$evt.value: $evt" sendMessage() } @@ -80,6 +80,6 @@ def sendMessage() { sendSms phone3, msg } if (!phone1 && !phone2 && !phone3) { - sendPush msg + sendPush msg } } From 97e0e9d0f815fdf5c6477b592b6d5c6a7e44c877 Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Thu, 25 Aug 2016 20:03:44 -0400 Subject: [PATCH 06/18] CREX-5826 - Replace text (#1153) --- .../notify-me-when.src/notify-me-when.groovy | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) 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 f124260..9a55db2 100644 --- a/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy +++ b/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy @@ -48,9 +48,9 @@ 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 + input "phone", "phone", title: "Enter a phone number to get SMS", 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"] + input "pushAndPhone", "enum", title: "Notify me via Push Notification", required: false, options: ["Yes", "No"] } } section("Minimum time between messages (optional, defaults to every message)") { @@ -111,19 +111,17 @@ private sendMessage(evt) { if (location.contactBookEnabled) { sendNotificationToContacts(msg, recipients, options) } else { - if (!phone || pushAndPhone != 'No') { + if (pushAndPhone != 'No') { log.debug 'sending push' options.method = 'push' - //sendPush(msg) + sendNotification(msg, options) } if (phone) { options.phone = phone log.debug 'sending SMS' - //sendSms(phone, msg) + sendNotification(msg, options) } - sendNotification(msg, options) } - if (frequency) { state[evt.deviceId] = now() } From 0744384dbfbc2c72be116486bec2456b92d2d038 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Thu, 25 Aug 2016 20:36:41 -0500 Subject: [PATCH 07/18] remove logging of token information --- .../smartthings/ecobee-connect.src/ecobee-connect.groovy | 5 ----- 1 file changed, 5 deletions(-) diff --git a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy index 5922812..d8c2179 100644 --- a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy +++ b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy @@ -128,9 +128,6 @@ def callback() { httpPost(uri: tokenUrl) { resp -> atomicState.refreshToken = resp.data.refresh_token atomicState.authToken = resp.data.access_token - log.debug "swapped token: $resp.data" - log.debug "atomicState.refreshToken: ${atomicState.refreshToken}" - log.debug "atomicState.authToken: ${atomicState.authToken}" } if (atomicState.authToken) { @@ -569,8 +566,6 @@ private refreshAuthToken() { query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId], ] - log.debug "making refresh request with paramaters: $refreshParams" - def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials." //changed to httpPost try { From 555a9f5ab4d42978fb17f5a67672843436d3f297 Mon Sep 17 00:00:00 2001 From: Parijat Das Date: Fri, 26 Aug 2016 19:10:19 +0530 Subject: [PATCH 08/18] Added troubleshooting component in README for the following devices: 1. SmartPower Outlet 2. SmartSense Moisture Sensor 3. SmartSense Motion Sensor 4. SmartSense Multi Sensor 5. SmartSense Open/Closed Sensor 6. SmartSense Temp-Humidity Sensor 7. Connected Cree LED Bulb --- devicetypes/smartthings/cree-bulb.src/README.md | 11 +++++++++-- .../smartthings/smartpower-outlet.src/README.md | 11 ++++++++++- .../smartsense-moisture-sensor.src/README.md | 13 ++++++++++++- .../smartsense-motion-sensor.src/README.md | 15 ++++++++++++++- .../smartsense-multi-sensor.src/README.md | 11 ++++++++++- .../smartsense-open-closed-sensor.src/README.md | 9 ++++++++- .../smartsense-temp-humidity-sensor.src/README.md | 9 ++++++++- 7 files changed, 71 insertions(+), 8 deletions(-) diff --git a/devicetypes/smartthings/cree-bulb.src/README.md b/devicetypes/smartthings/cree-bulb.src/README.md index bde78b1..2b8093c 100644 --- a/devicetypes/smartthings/cree-bulb.src/README.md +++ b/devicetypes/smartthings/cree-bulb.src/README.md @@ -4,7 +4,7 @@ Works with: -* [Samsung Connected Cree LED Bulb](https://support.smartthings.com/hc/en-us/articles/204258280-Cree-Connected-LED-Bulb) +* [Connected Cree LED Bulb](https://support.smartthings.com/hc/en-us/articles/204258280-Cree-Connected-LED-Bulb) ## Table of contents @@ -26,4 +26,11 @@ Works with: A Category C6 Connected Cree LED Bulb with maxReportTime of 10 min. Check-in interval is double the value of maxReportTime for Zigbee device. This gives the device twice the amount of time to respond before it is marked as offline. -Check-in interval = 2*10 = 20 min \ No newline at end of file +Check-in interval = 2*10 = 20 min + +## Troubleshooting + +If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range. +Pairing needs to be tried again by placing the device closer to the hub. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: +* [Cree Connected LED Bulb Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/204258280-Cree-Connected-LED-Bulb) \ No newline at end of file diff --git a/devicetypes/smartthings/smartpower-outlet.src/README.md b/devicetypes/smartthings/smartpower-outlet.src/README.md index 0a7739a..4dda7bf 100644 --- a/devicetypes/smartthings/smartpower-outlet.src/README.md +++ b/devicetypes/smartthings/smartpower-outlet.src/README.md @@ -26,4 +26,13 @@ Works with: A Category C1 smart power outlet with maxReportTime of 10 min. Check-in interval is double the value of maxReportTime for Zigbee device. This gives the device twice the amount of time to respond before it is marked as offline. -Check-in interval = 2*10 = 20 min \ No newline at end of file +Check-in interval = 2*10 = 20 min + +## Troubleshooting + +If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range. +Pairing needs to be tried again by placing the device closer to the hub. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following links +for the different models: +* [SmartPower Outlet Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/201084854-SmartPower-Outlet) +* [Samsung SmartThings Outlet Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/205957620) \ No newline at end of file diff --git a/devicetypes/smartthings/smartsense-moisture-sensor.src/README.md b/devicetypes/smartthings/smartsense-moisture-sensor.src/README.md index 0f1ac00..c346957 100644 --- a/devicetypes/smartthings/smartsense-moisture-sensor.src/README.md +++ b/devicetypes/smartthings/smartsense-moisture-sensor.src/README.md @@ -30,4 +30,15 @@ Check-in interval = 2*60 = 120 min ## Battery Specification -One CR2 3V battery required. \ No newline at end of file +One CR2 3V battery required. + +## Troubleshooting + +If the sensor doesn't pair when trying from the SmartThings mobile app, it is possible that the sensor is out of range. +Pairing needs to be tried again by placing the sensor closer to the hub. +Instructions related to pairing, resetting and removing the different sensors from SmartThings can be found in the following links +for the different models: +* [SmartSense Moisture Sensor Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/202847044-SmartSense-Moisture-Sensor) +* [Samsung SmartThings Water Leak Sensor Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/205957630) +Other troubleshooting tips are listed as follows: +* [Troubleshooting: Samsung SmartThings Water Leak Sensor won’t pair after removing pull-tab](https://support.smartthings.com/hc/en-us/articles/204966616-Troubleshooting-Samsung-SmartThings-device-won-t-pair-after-removing-pull-tab) diff --git a/devicetypes/smartthings/smartsense-motion-sensor.src/README.md b/devicetypes/smartthings/smartsense-motion-sensor.src/README.md index 0c8a9d3..d177cc6 100644 --- a/devicetypes/smartthings/smartsense-motion-sensor.src/README.md +++ b/devicetypes/smartthings/smartsense-motion-sensor.src/README.md @@ -29,4 +29,17 @@ Check-in interval = 2*60 = 120 min ## Battery Specification -One CR123A 3V battery is required. \ No newline at end of file +One CR2477 (for Samsung SmartThings Motion Sensor) / CR123A (SmartSense Motion Sensor) 3V battery is required. + +## Troubleshooting + +If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the sensor is out of range. +Pairing needs to be tried again by placing the sensor closer to the hub. +Instructions related to pairing, resetting and removing the different motion sensors from SmartThings can be found in the following links +for the different models: +* [SmartSense Motion Sensor (original model) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/200903280-SmartSense-Motion-Sensor-original-model-) +* [SmartSense Motion Sensor (2014 model) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/203077520-SmartSense-Motion-Sensor-2014-model-) +* [Samsung SmartThings Motion Sensor (2015 model) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/205957580-Samsung-SmartThings-Motion-Sensor-2015-model-) +Other troubleshooting tips are listed as follows: +* [Troubleshooting: Samsung SmartThings Motion Sensor is stuck showing "Motion Detected" or "No Motion"](https://support.smartthings.com/hc/en-us/articles/200961130-Troubleshooting-Samsung-SmartThings-Motion-Sensor-is-stuck-showing-Motion-Detected-or-No-Motion-) +* [Troubleshooting: Samsung SmartThings Motion Sensor won’t pair after removing pull-tab](https://support.smartthings.com/hc/en-us/articles/204966616-Troubleshooting-Samsung-SmartThings-device-won-t-pair-after-removing-pull-tab) diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/README.md b/devicetypes/smartthings/smartsense-multi-sensor.src/README.md index ccabf1d..809a192 100644 --- a/devicetypes/smartthings/smartsense-multi-sensor.src/README.md +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/README.md @@ -33,4 +33,13 @@ Check-in interval = 2*60 = 120 min ## Battery Specification -Two AAAA battery required. \ No newline at end of file +One CR2450 (for Samsung SmartThings Multipurpose Sensor) battery / Two AAAA (for SmartSense Multi Sensor) batteries required. + +## Troubleshooting + +If the sensor doesn't pair when trying from the SmartThings mobile app, it is possible that the sensor is out of range. +Pairing needs to be tried again by placing the sensor closer to the hub. +Other troubleshooting tips are listed as follows: +* [Troubleshooting: Samsung SmartThings Multipurpose Sensor is stuck on "open" or "closed"](https://support.smartthings.com/hc/en-us/articles/200955940-Troubleshooting-Samsung-SmartThings-Multipurpose-Sensor-is-stuck-on-open-or-closed-) +* [Troubleshooting: Temperature reading for the Samsung SmartThings Multipurpose Sensor is off](https://support.smartthings.com/hc/en-us/articles/200756845-Troubleshooting-Temperature-reading-for-the-Samsung-SmartThings-Multipurpose-Sensor-is-off) +* [Troubleshooting: Samsung SmartThings Multipurpose Sensor won’t pair after removing pull-tab](https://support.smartthings.com/hc/en-us/articles/204966616-Troubleshooting-Samsung-SmartThings-device-won-t-pair-after-removing-pull-tab) \ No newline at end of file diff --git a/devicetypes/smartthings/smartsense-open-closed-sensor.src/README.md b/devicetypes/smartthings/smartsense-open-closed-sensor.src/README.md index dd012e5..4cf3bf3 100644 --- a/devicetypes/smartthings/smartsense-open-closed-sensor.src/README.md +++ b/devicetypes/smartthings/smartsense-open-closed-sensor.src/README.md @@ -31,4 +31,11 @@ Check-in interval = 2*60 = 120 min ## Battery Specification -One CR2 3V battery required. \ No newline at end of file +One CR2 3V battery required. + +## Troubleshooting + +If the sensor doesn't pair when trying from the SmartThings mobile app, it is possible that the sensor is out of range. +Pairing needs to be tried again by placing the sensor closer to the hub. +Instructions related to pairing, resetting and removing the sensor from SmartThings can be found in the following link: +* [SmartSense Open/Closed Sensor Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/202836844-SmartSense-Open-Closed-Sensor) \ No newline at end of file diff --git a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/README.md b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/README.md index 7b2f0ac..3d6d3c8 100644 --- a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/README.md +++ b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/README.md @@ -31,4 +31,11 @@ Check-in interval = 2*60 = 120 min ## Battery Specification -One CR2 battery is required. \ No newline at end of file +One CR2 battery is required. + +## Troubleshooting + +If the sensor doesn't pair when trying from the SmartThings mobile app, it is possible that the sensor is out of range. +Pairing needs to be tried by placing the sensor closer to the hub. +Instructions related to pairing, resetting and removing the sensor from SmartThings can be found in the following link: +* [Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/203040294) \ No newline at end of file From b95ba3736416f553da86bc905fd52ace5ab44d13 Mon Sep 17 00:00:00 2001 From: Rohan Desai Date: Fri, 26 Aug 2016 11:12:41 -0700 Subject: [PATCH 09/18] removed log debugs of sensitive info --- .../life360-connect.src/life360-connect.groovy | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/smartapps/smartthings/life360-connect.src/life360-connect.groovy b/smartapps/smartthings/life360-connect.src/life360-connect.groovy index cf07750..c4f2b8e 100644 --- a/smartapps/smartthings/life360-connect.src/life360-connect.groovy +++ b/smartapps/smartthings/life360-connect.src/life360-connect.groovy @@ -74,8 +74,6 @@ def authPage() def redirectUrl = oauthInitUrl() - log.debug "RedirectURL = ${redirectUrl}" - return dynamicPage(name: "Credentials", title: "Life360", nextPage:"listCirclesPage", uninstall: uninstallOption, install:false) { section { href url:redirectUrl, style:"embedded", required:false, title:"Life360", description:description @@ -257,8 +255,6 @@ def initializeLife360Connection() { def oauthClientId = appSettings.clientId def oauthClientSecret = appSettings.clientSecret - log.debug "Installed with settings: ${settings}" - initialize() def username = settings.username @@ -269,8 +265,6 @@ def initializeLife360Connection() { def basicCredentials = "${oauthClientId}:${oauthClientSecret}" def encodedCredentials = basicCredentials.encodeAsBase64().toString() - log.debug "Encoded Creds: ${encodedCredentials}" - // call life360, get OAUTH token using password flow, save // curl -X POST -H "Authorization: Basic cFJFcXVnYWJSZXRyZTRFc3RldGhlcnVmcmVQdW1hbUV4dWNyRUh1YzptM2ZydXBSZXRSZXN3ZXJFQ2hBUHJFOTZxYWtFZHI0Vg==" @@ -284,8 +278,6 @@ def initializeLife360Connection() { "username=${username}&"+ "password=${password}" - log.debug "Post Body: ${postBody}" - def result = null try { @@ -295,7 +287,6 @@ def initializeLife360Connection() { } if (result.data.access_token) { state.life360AccessToken = result.data.access_token - log.debug "Access Token = ${state.life360AccessToken}" return true; } log.debug "Response=${result.data}" @@ -533,8 +524,6 @@ def createCircleSubscription() { def postBody = "url=${hookUrl}" - log.debug "Post Body: ${postBody}" - def result = null try { @@ -586,8 +575,6 @@ def updated() { // log.debug "After Find Attempt." - log.debug "Member Id = ${member.id}, Name = ${member.firstName} ${member.lastName}, Email Address = ${member.loginEmail}" - // log.debug "External Id=${app.id}:${member.id}" // create the device From 212c9c4179353538f95b2c5bf2cdfe2a54abce2a Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Fri, 26 Aug 2016 15:17:02 -0700 Subject: [PATCH 10/18] adding support for osram tw b40 --- .../zigbee-white-color-temperature-bulb.groovy | 1 + 1 file changed, 1 insertion(+) diff --git a/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy index 2154599..de8424d 100644 --- a/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy +++ b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy @@ -36,6 +36,7 @@ metadata { fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY RT Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Recessed Kit RT 5/6 Tunable White" fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 TW", deviceJoinName: "OSRAM LIGHTIFY LED Tunable White 60W" fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Tunable White 60W" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic B40 TW - LIGHTIFY", deviceJoinName: "OSRAM LIGHTIFY Classic B40 Tunable White" } // UI tile definitions From 1f8ce734e72f23bdf2e60e9bc56f7184f8a79084 Mon Sep 17 00:00:00 2001 From: marstorp Date: Mon, 29 Aug 2016 10:20:04 -0700 Subject: [PATCH 11/18] DVCSMP-1954 Routines turning Bose speaker on/off, toggles the power state to off/on --- .../bose-soundtouch.groovy | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/devicetypes/smartthings/bose-soundtouch.src/bose-soundtouch.groovy b/devicetypes/smartthings/bose-soundtouch.src/bose-soundtouch.groovy index 2d60bf8..38d624c 100644 --- a/devicetypes/smartthings/bose-soundtouch.src/bose-soundtouch.groovy +++ b/devicetypes/smartthings/bose-soundtouch.src/bose-soundtouch.groovy @@ -47,6 +47,9 @@ metadata { command "everywhereJoin" command "everywhereLeave" + + command "forceOff" + command "forceOn" } /** @@ -64,9 +67,9 @@ metadata { } standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) { - state "on", label: '${name}', action: "switch.off", icon: "st.Electronics.electronics16", backgroundColor: "#79b821", nextState:"turningOff" + state "on", label: '${name}', action: "forceOff", icon: "st.Electronics.electronics16", backgroundColor: "#79b821", nextState:"turningOff" state "turningOff", label:'TURNING OFF', icon:"st.Electronics.electronics16", backgroundColor:"#ffffff" - state "off", label: '${name}', action: "switch.on", icon: "st.Electronics.electronics16", backgroundColor: "#ffffff", nextState:"turningOn" + state "off", label: '${name}', action: "forceOn", icon: "st.Electronics.electronics16", backgroundColor: "#ffffff", nextState:"turningOn" state "turningOn", label:'TURNING ON', icon:"st.Electronics.electronics16", backgroundColor:"#79b821" } valueTile("1", "device.station1", decoration: "flat", canChangeIcon: false) { @@ -140,8 +143,22 @@ metadata { * one place. * */ -def off() { onAction("off") } -def on() { onAction("on") } +def off() { + if (device.currentState("switch")?.value == "on") { + onAction("off") + } +} +def forceOff() { + onAction("off") +} +def on() { + if (device.currentState("switch")?.value == "off") { + onAction("on") + } +} +def forceOn() { + onAction("on") +} def volup() { onAction("volup") } def voldown() { onAction("voldown") } def preset1() { onAction("1") } @@ -240,11 +257,11 @@ def onAction(String user, data=null) { def actions = null switch (user) { case "on": - actions = boseSetPowerState(true) + boseSetPowerState(true) break case "off": boseSetNowPlaying(null, "STANDBY") - actions = boseSetPowerState(false) + boseSetPowerState(false) break case "volume": actions = boseSetVolume(data) From beed783d191fbe4eff47026e3bc7e7a8e5296d43 Mon Sep 17 00:00:00 2001 From: Mike Robinet Date: Fri, 26 Aug 2016 15:56:47 -0500 Subject: [PATCH 12/18] SSVD-2534 Set temperature unit/scale when sending temperature events --- .../keen-home-smart-vent.groovy | 1 + .../spruce-sensor.src/spruce-sensor.groovy | 3 ++- .../centralite-thermostat.groovy | 7 +++-- .../ecobee-thermostat.groovy | 26 +++++++++---------- .../fidure-thermostat.groovy | 4 +-- .../smartsense-moisture-sensor.groovy | 3 ++- .../smartsense-motion-sensor.groovy | 3 ++- .../smartsense-motion-temp-sensor.groovy | 3 ++- .../smartsense-multi-sensor.groovy | 9 ++++--- ...se-open-closed-accelerometer-sensor.groovy | 7 ++--- .../smartsense-open-closed-sensor.groovy | 3 ++- .../smartsense-temp-humidity-sensor.groovy | 3 ++- .../tyco-door-window-sensor.groovy | 3 ++- 13 files changed, 44 insertions(+), 31 deletions(-) diff --git a/devicetypes/keen-home/keen-home-smart-vent.src/keen-home-smart-vent.groovy b/devicetypes/keen-home/keen-home-smart-vent.src/keen-home-smart-vent.groovy index 232378a..9f64b2c 100644 --- a/devicetypes/keen-home/keen-home-smart-vent.src/keen-home-smart-vent.groovy +++ b/devicetypes/keen-home/keen-home-smart-vent.src/keen-home-smart-vent.groovy @@ -274,6 +274,7 @@ private Map makeTemperatureResult(value) { name: 'temperature', value: "" + value, descriptionText: "${linkText} is ${value}°${temperatureScale}", + unit: temperatureScale ] } diff --git a/devicetypes/plaidsystems/spruce-sensor.src/spruce-sensor.groovy b/devicetypes/plaidsystems/spruce-sensor.src/spruce-sensor.groovy index 944ca52..77a5698 100644 --- a/devicetypes/plaidsystems/spruce-sensor.src/spruce-sensor.groovy +++ b/devicetypes/plaidsystems/spruce-sensor.src/spruce-sensor.groovy @@ -254,7 +254,8 @@ private Map getTemperatureResult(value) { return [ name: 'temperature', value: value, - descriptionText: descriptionText + descriptionText: descriptionText, + unit: temperatureScale ] } diff --git a/devicetypes/smartthings/centralite-thermostat.src/centralite-thermostat.groovy b/devicetypes/smartthings/centralite-thermostat.src/centralite-thermostat.groovy index b6c69a4..a92839f 100644 --- a/devicetypes/smartthings/centralite-thermostat.src/centralite-thermostat.groovy +++ b/devicetypes/smartthings/centralite-thermostat.src/centralite-thermostat.groovy @@ -89,14 +89,17 @@ def parse(String description) { log.debug "TEMP" map.name = "temperature" map.value = getTemperature(descMap.value) + map.unit = temperatureScale } else if (descMap.cluster == "0201" && descMap.attrId == "0011") { log.debug "COOLING SETPOINT" map.name = "coolingSetpoint" map.value = getTemperature(descMap.value) + map.unit = temperatureScale } else if (descMap.cluster == "0201" && descMap.attrId == "0012") { log.debug "HEATING SETPOINT" map.name = "heatingSetpoint" map.value = getTemperature(descMap.value) + map.unit = temperatureScale } else if (descMap.cluster == "0201" && descMap.attrId == "001c") { log.debug "MODE" map.name = "thermostatMode" @@ -169,7 +172,7 @@ def setHeatingSetpoint(degrees) { def degreesInteger = Math.round(degrees) log.debug "setHeatingSetpoint({$degreesInteger} ${temperatureScale})" - sendEvent("name": "heatingSetpoint", "value": degreesInteger) + sendEvent("name": "heatingSetpoint", "value": degreesInteger, "unit": temperatureScale) def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2) "st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius * 100) + "}" @@ -180,7 +183,7 @@ def setCoolingSetpoint(degrees) { if (degrees != null) { def degreesInteger = Math.round(degrees) log.debug "setCoolingSetpoint({$degreesInteger} ${temperatureScale})" - sendEvent("name": "coolingSetpoint", "value": degreesInteger) + sendEvent("name": "coolingSetpoint", "value": degreesInteger, "unit": temperatureScale) def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2) "st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius * 100) + "}" } diff --git a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy index 2705f56..a16d28c 100644 --- a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy +++ b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy @@ -152,11 +152,11 @@ def generateEvent(Map results) { sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue isChange = isTemperatureStateChange(device, name, value.toString()) isDisplayed = isChange - event << [value: sendValue, isStateChange: isChange, displayed: isDisplayed] + event << [value: sendValue, unit: temperatureScale, isStateChange: isChange, displayed: isDisplayed] } else if (name=="maxCoolingSetpoint" || name=="minCoolingSetpoint" || name=="maxHeatingSetpoint" || name=="minHeatingSetpoint") { def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue - event << [value: sendValue, displayed: false] + event << [value: sendValue, unit: temperatureScale, displayed: false] } else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){ isChange = isStateChange(device, name, value.toString()) event << [value: value.toString(), isStateChange: isChange, displayed: false] @@ -235,8 +235,8 @@ void setHeatingSetpoint(setpoint) { def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite" if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) { - sendEvent("name":"heatingSetpoint", "value":heatingSetpoint) - sendEvent("name":"coolingSetpoint", "value":coolingSetpoint) + sendEvent("name":"heatingSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale) + sendEvent("name":"coolingSetpoint", "value":coolingSetpoint, "unit":location.temperatureScale) log.debug "Done setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}" generateSetpointEvent() generateStatusEvent() @@ -272,8 +272,8 @@ void setCoolingSetpoint(setpoint) { def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite" if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) { - sendEvent("name":"heatingSetpoint", "value":heatingSetpoint) - sendEvent("name":"coolingSetpoint", "value":coolingSetpoint) + sendEvent("name":"heatingSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale) + sendEvent("name":"coolingSetpoint", "value":coolingSetpoint, "unit":location.temperatureScale) log.debug "Done setCoolingSetpoint>> coolingSetpoint = ${coolingSetpoint}, heatingSetpoint = ${heatingSetpoint}" generateSetpointEvent() generateStatusEvent() @@ -556,12 +556,12 @@ def generateSetpointEvent() { if (mode == "heat") { - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint ) + sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale) } else if (mode == "cool") { - sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint) + sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint, "unit":location.temperatureScale) } else if (mode == "auto") { @@ -573,7 +573,7 @@ def generateSetpointEvent() { } else if (mode == "auxHeatOnly") { - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint) + sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale) } @@ -608,7 +608,7 @@ void raiseSetpoint() { targetvalue = maxCoolingSetpoint } - sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false) + sendEvent("name":"thermostatSetpoint", "value":targetvalue, "unit":location.temperatureScale, 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 @@ -644,7 +644,7 @@ void lowerSetpoint() { targetvalue = minCoolingSetpoint } - sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false) + sendEvent("name":"thermostatSetpoint", "value":targetvalue, "unit":location.temperatureScale, 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 @@ -692,8 +692,8 @@ void alterSetpoint(temp) { if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) { sendEvent("name": "thermostatSetpoint", "value": temp.value, displayed: false) - sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint) - sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint) + sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint, "unit": location.temperatureScale) + sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint, "unit": location.temperatureScale) log.debug "alterSetpoint in mode $mode succeed change setpoint to= ${temp.value}" } else { log.error "Error alterSetpoint()" diff --git a/devicetypes/smartthings/fidure-thermostat.src/fidure-thermostat.groovy b/devicetypes/smartthings/fidure-thermostat.src/fidure-thermostat.groovy index 6de1a36..a731447 100644 --- a/devicetypes/smartthings/fidure-thermostat.src/fidure-thermostat.groovy +++ b/devicetypes/smartthings/fidure-thermostat.src/fidure-thermostat.groovy @@ -682,7 +682,7 @@ def setHeatingSetpoint(degrees) { def temperatureScale = getTemperatureScale() def degreesInteger = degrees as Integer - sendEvent("name":"heatingSetpoint", "value":degreesInteger) + sendEvent("name":"heatingSetpoint", "value":degreesInteger, "unit":temperatureScale) def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2) "st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius*100) + "}" @@ -691,7 +691,7 @@ def setHeatingSetpoint(degrees) { def setCoolingSetpoint(degrees) { def degreesInteger = degrees as Integer - sendEvent("name":"coolingSetpoint", "value":degreesInteger) + sendEvent("name":"coolingSetpoint", "value":degreesInteger, "unit":temperatureScale) def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2) "st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius*100) + "}" diff --git a/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy b/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy index 7de68d1..2c859da 100644 --- a/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy +++ b/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy @@ -259,7 +259,8 @@ private Map getTemperatureResult(value) { name: 'temperature', value: value, descriptionText: descriptionText, - translatable: true + translatable: true, + unit: temperatureScale ] } diff --git a/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy b/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy index bb9acb7..2a6751e 100644 --- a/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy +++ b/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy @@ -274,7 +274,8 @@ private Map getTemperatureResult(value) { name: 'temperature', value: value, descriptionText: descriptionText, - translatable: true + translatable: true, + unit: temperatureScale ] } diff --git a/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy b/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy index f5ebc5f..8e6990f 100644 --- a/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy +++ b/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy @@ -226,7 +226,8 @@ private Map getTemperatureResult(value) { return [ name: 'temperature', value: value, - descriptionText: descriptionText + descriptionText: descriptionText, + unit: temperatureScale ] } 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 ad3c60d..735312b 100644 --- a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy @@ -333,10 +333,11 @@ private Map getTemperatureResult(value) { '{{ device.displayName }} was {{ value }}°F' return [ - name: 'temperature', - value: value, - descriptionText: descriptionText, - translatable: true + name: 'temperature', + value: value, + descriptionText: descriptionText, + translatable: true, + unit: temperatureScale ] } diff --git a/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy b/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy index ac82584..f8ff1e0 100644 --- a/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy +++ b/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy @@ -223,9 +223,10 @@ def getTemperature(value) { } def descriptionText = "${linkText} was ${value}°${temperatureScale}" return [ - name: 'temperature', - value: value, - descriptionText: descriptionText + name: 'temperature', + value: value, + descriptionText: descriptionText, + unit: temperatureScale ] } diff --git a/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy b/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy index f9b24e8..3cbac84 100644 --- a/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy +++ b/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy @@ -226,7 +226,8 @@ private Map getTemperatureResult(value) { return [ name: 'temperature', value: value, - descriptionText: descriptionText + descriptionText: descriptionText, + unit: temperatureScale ] } diff --git a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy index cbdb78b..00e660c 100644 --- a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy +++ b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy @@ -233,7 +233,8 @@ private Map getTemperatureResult(value) { return [ name: 'temperature', value: value, - descriptionText: descriptionText + descriptionText: descriptionText, + unit: temperatureScale ] } diff --git a/devicetypes/smartthings/tyco-door-window-sensor.src/tyco-door-window-sensor.groovy b/devicetypes/smartthings/tyco-door-window-sensor.src/tyco-door-window-sensor.groovy index 4121630..e8f1e46 100644 --- a/devicetypes/smartthings/tyco-door-window-sensor.src/tyco-door-window-sensor.groovy +++ b/devicetypes/smartthings/tyco-door-window-sensor.src/tyco-door-window-sensor.groovy @@ -213,7 +213,8 @@ private Map getTemperatureResult(value) { return [ name: 'temperature', value: value, - descriptionText: descriptionText + descriptionText: descriptionText, + unit: temperatureScale ] } From bacd335991fcef93b0df24acae3a36510bdd427b Mon Sep 17 00:00:00 2001 From: Lars Finander Date: Tue, 30 Aug 2016 17:58:27 -0600 Subject: [PATCH 13/18] WWST-40 Philips Hue: Implement device watch --- .../hue-bloom.src/hue-bloom.groovy | 9 ++ .../smartthings/hue-bulb.src/hue-bulb.groovy | 9 ++ .../hue-lux-bulb.src/hue-lux-bulb.groovy | 9 ++ .../hue-white-ambiance-bulb.groovy | 9 ++ .../hue-connect.src/hue-connect.groovy | 141 ++++++++++++------ 5 files changed, 130 insertions(+), 47 deletions(-) diff --git a/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy b/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy index f99f436..9f7d804 100644 --- a/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy +++ b/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy @@ -16,6 +16,7 @@ metadata { capability "Switch" capability "Refresh" capability "Sensor" + capability "Health Check" command "setAdjustedColor" command "reset" @@ -55,6 +56,10 @@ metadata { } } +void installed() { + sendEvent(name: "checkInterval", value: 60 * 30, data: [protocol: "lan"], displayed: false) +} + // parse events into attributes def parse(description) { log.debug "parse() - $description" @@ -166,3 +171,7 @@ def verifyPercent(percent) { return false } } + +def ping() { + log.debug "${parent.ping(this)}" +} diff --git a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy index 0c3d917..1f63b09 100644 --- a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy +++ b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy @@ -17,6 +17,7 @@ metadata { capability "Switch" capability "Refresh" capability "Sensor" + capability "Health Check" command "setAdjustedColor" command "reset" @@ -64,6 +65,10 @@ metadata { } } +void installed() { + sendEvent(name: "checkInterval", value: 60 * 30, data: [protocol: "lan"], displayed: false) +} + // parse events into attributes def parse(description) { log.debug "parse() - $description" @@ -182,3 +187,7 @@ def verifyPercent(percent) { return false } } + +def ping() { + log.trace "${parent.ping(this)}" +} diff --git a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy index 728b265..b1f076d 100644 --- a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy +++ b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy @@ -14,6 +14,7 @@ metadata { capability "Switch" capability "Refresh" capability "Sensor" + capability "Health Check" command "refresh" } @@ -48,6 +49,10 @@ metadata { } } +void installed() { + sendEvent(name: "checkInterval", value: 60 * 30, data: [protocol: "lan"], displayed: false) +} + // parse events into attributes def parse(description) { log.debug "parse() - $description" @@ -87,3 +92,7 @@ void refresh() { log.debug "Executing 'refresh'" parent.manualRefresh() } + +def ping() { + log.debug "${parent.ping(this)}" +} diff --git a/devicetypes/smartthings/hue-white-ambiance-bulb.src/hue-white-ambiance-bulb.groovy b/devicetypes/smartthings/hue-white-ambiance-bulb.src/hue-white-ambiance-bulb.groovy index 0a25741..fd8c8bf 100644 --- a/devicetypes/smartthings/hue-white-ambiance-bulb.src/hue-white-ambiance-bulb.groovy +++ b/devicetypes/smartthings/hue-white-ambiance-bulb.src/hue-white-ambiance-bulb.groovy @@ -15,6 +15,7 @@ metadata { capability "Color Temperature" capability "Switch" capability "Refresh" + capability "Health Check" command "refresh" } @@ -53,6 +54,10 @@ metadata { } } +void installed() { + sendEvent(name: "checkInterval", value: 60 * 30, data: [protocol: "lan"], displayed: false) +} + // parse events into attributes def parse(description) { log.debug "parse() - $description" @@ -101,3 +106,7 @@ void refresh() { log.debug "Executing 'refresh'" parent.manualRefresh() } + +def ping() { + log.debug "${parent.ping(this)}" +} diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index e62d363..7a6e8a3 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -95,8 +95,7 @@ def bridgeDiscoveryFailed() { } } -def bridgeLinking() -{ +def bridgeLinking() { int linkRefreshcount = !state.linkRefreshcount ? 0 : state.linkRefreshcount as int state.linkRefreshcount = linkRefreshcount + 1 def refreshInterval = 3 @@ -328,7 +327,7 @@ def bulbListHandler(hub, data = "") { def object = new groovy.json.JsonSlurper().parseText(data) object.each { k,v -> if (v instanceof Map) - bulbs[k] = [id: k, name: v.name, type: v.type, modelid: v.modelid, hub:hub] + bulbs[k] = [id: k, name: v.name, type: v.type, modelid: v.modelid, hub:hub, online: v.state?.reachable] } } def bridge = null @@ -448,7 +447,6 @@ def addBridge() { updateBridgeStatus(childDevice) childDevice.sendEvent(name: "idNumber", value: idNumber) - if (vbridge.value.ip && vbridge.value.port) { if (vbridge.value.ip.contains(".")) { childDevice.sendEvent(name: "networkAddress", value: vbridge.value.ip + ":" + vbridge.value.port) @@ -649,8 +647,7 @@ def locationHandler(evt) { } } } - } - else if (parsedEvent.headers && parsedEvent.body) { + } else if (parsedEvent.headers && parsedEvent.body) { log.trace "HUE BRIDGE RESPONSES" def headerString = parsedEvent.headers.toString() if (headerString?.contains("xml")) { @@ -733,7 +730,7 @@ private void updateBridgeStatus(childDevice) { private void checkBridgeStatus() { def bridges = getHueBridges() // Check if each bridge has been heard from within the last 16 minutes (3 poll intervals times 5 minutes plus buffer) - def time = now() - (1000 * 60 * 16) + def time = now() - (1000 * 60 * 30) bridges.each { def d = getChildDevice(it.value.mac) if(d) { @@ -746,6 +743,8 @@ private void checkBridgeStatus() { if (it.value.lastActivity < time) { // it.value.lastActivity != null && log.warn "Bridge $it.key is Offline" d.sendEvent(name: "status", value: "Offline") + // set all lights to offline since bridge is not reachable + state.bulbs?.each {it.value.online = false} } else { d.sendEvent(name: "status", value: "Online")//setOnline(false) } @@ -781,8 +780,7 @@ def parse(childDevice, description) { if (body instanceof java.util.Map) { // get (poll) reponse return handlePoll(body) - } - else { + } else { //put response return handleCommandResponse(body) } @@ -879,36 +877,40 @@ private handleCommandResponse(body) { // scan entire response before sending events to make sure they are always in the same order def updates = [:] - body.each { payload -> - log.debug $payload + body.each { payload -> + log.debug $payload if (payload?.success) { - def childDeviceNetworkId = app.id + "/" - def eventType + def childDeviceNetworkId = app.id + "/" + def eventType payload.success.each { k, v -> def data = k.split("/") if (data.length == 5) { childDeviceNetworkId = app.id + "/" + k.split("/")[2] if (!updates[childDeviceNetworkId]) updates[childDeviceNetworkId] = [:] - eventType = k.split("/")[4] + eventType = k.split("/")[4] updates[childDeviceNetworkId]."$eventType" = v } } } else if (payload.error) { log.warn "Error returned from Hue bridge error = ${body?.error}" - } - } + } + } // send events for each update found above (order of events should be same as handlePoll()) updates.each { childDeviceNetworkId, params -> def device = getChildDevice(childDeviceNetworkId) - sendBasicEvents(device, "on", params.on) - sendBasicEvents(device, "bri", params.bri) - sendColorEvents(device, params.xy, params.hue, params.sat, params.ct) - } + def id = getId(device) + // If device is offline, then don't send events which will update device watch + if (isOnline(id)) { + sendBasicEvents(device, "on", params.on) + sendBasicEvents(device, "bri", params.bri) + sendColorEvents(device, params.xy, params.hue, params.sat, params.ct) + } + } return [] - } +} /** * Handles a response to a poll (GET) sent to the Hue Bridge. @@ -928,26 +930,32 @@ private handleCommandResponse(body) { * @return empty array */ private handlePoll(body) { - if (state.updating) { - // If user just executed commands, then ignore poll to not confuse the turning on/off state - return [] - } - def bulbs = getChildDevices() for (bulb in body) { def device = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"} if (device) { if (bulb.value.state?.reachable) { - sendBasicEvents(device, "on", bulb.value?.state?.on) - sendBasicEvents(device, "bri", bulb.value?.state?.bri) - sendColorEvents(device, bulb.value?.state?.xy, bulb.value?.state?.hue, bulb.value?.state?.sat, bulb.value?.state?.ct, bulb.value?.state?.colormode) + if (state.bulbs[bulb.key]?.online == false) { + // light just came back online, notify device watch + def lastActivity = now() + device.sendEvent(name: "deviceWatch-status", value: "ONLINE", description: "Last Activity is on ${new Date((long) lastActivity)}", displayed: false, isStateChange: true) + } + state.bulbs[bulb.key]?.online = true + + // If user just executed commands, then do not send events to avoid confusing the turning on/off state + if (!state.updating) { + sendBasicEvents(device, "on", bulb.value?.state?.on) + sendBasicEvents(device, "bri", bulb.value?.state?.bri) + sendColorEvents(device, bulb.value?.state?.xy, bulb.value?.state?.hue, bulb.value?.state?.sat, bulb.value?.state?.ct, bulb.value?.state?.colormode) + } } else { + state.bulbs[bulb.key]?.online = false log.warn "$device is not reachable by Hue bridge" - } - } - } - return [] + } + } } + return [] +} private updateInProgress() { state.updating = true @@ -976,22 +984,34 @@ def hubVerification(bodytext) { def on(childDevice) { log.debug "Executing 'on'" + def id = getId(childDevice) + if (!isOnline(id)) { + return "Bulb is unreachable" + } updateInProgress() createSwitchEvent(childDevice, "on") - put("lights/${getId(childDevice)}/state", [on: true]) + put("lights/$id/state", [on: true]) return "Bulb is turning On" } def off(childDevice) { log.debug "Executing 'off'" + def id = getId(childDevice) + if (!isOnline(id)) { + return "Bulb is unreachable" + } updateInProgress() createSwitchEvent(childDevice, "off") - put("lights/${getId(childDevice)}/state", [on: false]) + put("lights/$id/state", [on: false]) return "Bulb is turning Off" } def setLevel(childDevice, percent) { log.debug "Executing 'setLevel'" + def id = getId(childDevice) + if (!isOnline(id)) { + return "Bulb is unreachable" + } updateInProgress() // 1 - 254 def level @@ -1006,48 +1026,64 @@ def setLevel(childDevice, percent) { // that means that the light will still be on when on is called next time // Lets emulate that here if (percent > 0) { - put("lights/${getId(childDevice)}/state", [bri: level, on: true]) + put("lights/$id/state", [bri: level, on: true]) } else { - put("lights/${getId(childDevice)}/state", [on: false]) + put("lights/$id/state", [on: false]) } return "Setting level to $percent" } def setSaturation(childDevice, percent) { log.debug "Executing 'setSaturation($percent)'" - updateInProgress() + def id = getId(childDevice) + if (!isOnline(id)) { + return "Bulb is unreachable" + } + + updateInProgress() // 0 - 254 def level = Math.min(Math.round(percent * 254 / 100), 254) // TODO should this be done by app only or should we default to on? createSwitchEvent(childDevice, "on") - put("lights/${getId(childDevice)}/state", [sat: level, on: true]) + put("lights/$id/state", [sat: level, on: true]) return "Setting saturation to $percent" } def setHue(childDevice, percent) { log.debug "Executing 'setHue($percent)'" + def id = getId(childDevice) + if (!isOnline(id)) { + return "Bulb is unreachable" + } updateInProgress() // 0 - 65535 def level = Math.min(Math.round(percent * 65535 / 100), 65535) // TODO should this be done by app only or should we default to on? createSwitchEvent(childDevice, "on") - put("lights/${getId(childDevice)}/state", [hue: level, on: true]) + put("lights/$id/state", [hue: level, on: true]) return "Setting hue to $percent" } def setColorTemperature(childDevice, huesettings) { log.debug "Executing 'setColorTemperature($huesettings)'" + def id = getId(childDevice) + if (!isOnline(id)) { + return "Bulb is unreachable" + } updateInProgress() // 153 (6500K) to 500 (2000K) def ct = hueSettings == 6500 ? 153 : Math.round(1000000/huesettings) createSwitchEvent(childDevice, "on") - put("lights/${getId(childDevice)}/state", [ct: ct, on: true]) + put("lights/$id/state", [ct: ct, on: true]) return "Setting color temperature to $percent" } def setColor(childDevice, huesettings) { log.debug "Executing 'setColor($huesettings)'" - + def id = getId(childDevice) + if (!isOnline(id)) { + return "Bulb is unreachable" + } updateInProgress() def value = [:] @@ -1104,15 +1140,23 @@ def setColor(childDevice, huesettings) { value.on = false createSwitchEvent(childDevice, value.on ? "on" : "off") - put("lights/${getId(childDevice)}/state", value) + put("lights/$id/state", value) return "Setting color to $value" } +def ping(childDevice) { + if (isOnline(getId(childDevice))) { + childDevice.sendEvent(name: "deviceWatch-ping", value: "ONLINE", description: "Hue Light is reachable", displayed: false, isStateChange: true) + return "Device is Online" + } else { + return "Device is Offline" + } +} + private getId(childDevice) { if (childDevice.device?.deviceNetworkId?.startsWith("HUE")) { return childDevice.device?.deviceNetworkId[3..-1] - } - else { + } else { return childDevice.device?.deviceNetworkId.split("/")[-1] } } @@ -1123,10 +1167,13 @@ private poll() { log.debug "GET: $host$uri" sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1 HOST: ${host} - """, physicalgraph.device.Protocol.LAN, selectedHue)) } +private isOnline(id) { + return (state.bulbs[id].online != null && state.bulbs[id].online) || state.bulbs[id].online == null +} + private put(path, body) { def host = getBridgeIP() def uri = "/api/${state.username}/$path" @@ -1194,7 +1241,7 @@ def convertBulbListToMap() { if (state.bulbs instanceof java.util.List) { def map = [:] state.bulbs.unique {it.id}.each { bulb -> - map << ["${bulb.id}":["id":bulb.id, "name":bulb.name, "type": bulb.type, "modelid": bulb.modelid, "hub":bulb.hub]] + map << ["${bulb.id}":["id":bulb.id, "name":bulb.name, "type": bulb.type, "modelid": bulb.modelid, "hub":bulb.hub, "online": bulb.online]] } state.bulbs = map } From af2ea04442726085e6c4726057813c1a502845ed Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Tue, 6 Sep 2016 12:04:26 -0400 Subject: [PATCH 14/18] CREX-5826 - 2 SMS bug fix (#1193) options.method = 'none' & variable name update Revert variable name --- .../notify-me-when.src/notify-me-when.groovy | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) 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 9a55db2..1d8a055 100644 --- a/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy +++ b/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy @@ -111,16 +111,23 @@ private sendMessage(evt) { if (location.contactBookEnabled) { sendNotificationToContacts(msg, recipients, options) } else { - if (pushAndPhone != 'No') { - log.debug 'sending push' - options.method = 'push' - sendNotification(msg, options) - } if (phone) { options.phone = phone - log.debug 'sending SMS' - sendNotification(msg, options) + if (pushAndPhone != 'No') { + log.debug 'Sending push and SMS' + options.method = 'both' + else { + log.debug 'Sending SMS' + options.method = 'phone' + } + else if (pushAndPhone != 'No') { + log.debug 'Sending push' + options.method = 'push' + } else { + log.debug 'Sending nothing' + options.method = 'none' } + sendNotification(msg, options) } if (frequency) { state[evt.deviceId] = now() From 26df619b4f8ae9d4cb32e39632d3696b27a38a63 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Tue, 6 Sep 2016 11:22:06 -0500 Subject: [PATCH 15/18] revert changes for DVCSMP-1980 and SSVD-2534 on staging --- .../ecobee-thermostat.groovy | 50 +- .../ecobee-connect.src/ecobee-connect.groovy | 642 ++++++++---------- 2 files changed, 306 insertions(+), 386 deletions(-) diff --git a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy index a16d28c..7b08b14 100644 --- a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy +++ b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy @@ -152,11 +152,11 @@ def generateEvent(Map results) { sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue isChange = isTemperatureStateChange(device, name, value.toString()) isDisplayed = isChange - event << [value: sendValue, unit: temperatureScale, isStateChange: isChange, displayed: isDisplayed] + event << [value: sendValue, isStateChange: isChange, displayed: isDisplayed] } else if (name=="maxCoolingSetpoint" || name=="minCoolingSetpoint" || name=="maxHeatingSetpoint" || name=="minHeatingSetpoint") { def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue - event << [value: sendValue, unit: temperatureScale, displayed: false] + event << [value: sendValue, displayed: false] } else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){ isChange = isStateChange(device, name, value.toString()) event << [value: value.toString(), isStateChange: isChange, displayed: false] @@ -234,9 +234,9 @@ void setHeatingSetpoint(setpoint) { def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite" - if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) { - sendEvent("name":"heatingSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale) - sendEvent("name":"coolingSetpoint", "value":coolingSetpoint, "unit":location.temperatureScale) + if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) { + sendEvent("name":"heatingSetpoint", "value":heatingSetpoint) + sendEvent("name":"coolingSetpoint", "value":coolingSetpoint) log.debug "Done setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}" generateSetpointEvent() generateStatusEvent() @@ -271,9 +271,9 @@ void setCoolingSetpoint(setpoint) { def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite" - if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) { - sendEvent("name":"heatingSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale) - sendEvent("name":"coolingSetpoint", "value":coolingSetpoint, "unit":location.temperatureScale) + if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) { + sendEvent("name":"heatingSetpoint", "value":heatingSetpoint) + sendEvent("name":"coolingSetpoint", "value":coolingSetpoint) log.debug "Done setCoolingSetpoint>> coolingSetpoint = ${coolingSetpoint}, heatingSetpoint = ${heatingSetpoint}" generateSetpointEvent() generateStatusEvent() @@ -287,14 +287,14 @@ void resumeProgram() { log.debug "resumeProgram() is called" sendEvent("name":"thermostatStatus", "value":"resuming schedule", "description":statusText, displayed: false) def deviceId = device.deviceNetworkId.split(/\./).last() - if (parent.resumeProgram(deviceId)) { + 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(deviceId)" + log.error "Error resumeProgram() check parent.resumeProgram(this, deviceId)" } } @@ -406,7 +406,7 @@ def generateOperatingStateEvent(operatingState) { def off() { log.debug "off" def deviceId = device.deviceNetworkId.split(/\./).last() - if (parent.setMode ("off", deviceId)) + if (parent.setMode (this,"off", deviceId)) generateModeEvent("off") else { log.debug "Error setting new mode." @@ -420,7 +420,7 @@ def off() { def heat() { log.debug "heat" def deviceId = device.deviceNetworkId.split(/\./).last() - if (parent.setMode ("heat", deviceId)) + if (parent.setMode (this,"heat", deviceId)) generateModeEvent("heat") else { log.debug "Error setting new mode." @@ -438,7 +438,7 @@ def emergencyHeat() { def auxHeatOnly() { log.debug "auxHeatOnly" def deviceId = device.deviceNetworkId.split(/\./).last() - if (parent.setMode ("auxHeatOnly", deviceId)) + if (parent.setMode (this,"auxHeatOnly", deviceId)) generateModeEvent("auxHeatOnly") else { log.debug "Error setting new mode." @@ -452,7 +452,7 @@ def auxHeatOnly() { def cool() { log.debug "cool" def deviceId = device.deviceNetworkId.split(/\./).last() - if (parent.setMode ("cool", deviceId)) + if (parent.setMode (this,"cool", deviceId)) generateModeEvent("cool") else { log.debug "Error setting new mode." @@ -466,7 +466,7 @@ def cool() { def auto() { log.debug "auto" def deviceId = device.deviceNetworkId.split(/\./).last() - if (parent.setMode ("auto", deviceId)) + if (parent.setMode (this,"auto", deviceId)) generateModeEvent("auto") else { log.debug "Error setting new mode." @@ -489,7 +489,7 @@ def fanOn() { def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint - if (parent.setFanMode(heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) { + if (parent.setFanMode(this, heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) { generateFanModeEvent(fanMode) } else { log.debug "Error setting new mode." @@ -510,7 +510,7 @@ def fanAuto() { def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint - if (parent.setFanMode(heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) { + if (parent.setFanMode(this, heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) { generateFanModeEvent(fanMode) } else { log.debug "Error setting new mode." @@ -556,12 +556,12 @@ def generateSetpointEvent() { if (mode == "heat") { - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale) + sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint ) } else if (mode == "cool") { - sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint, "unit":location.temperatureScale) + sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint) } else if (mode == "auto") { @@ -573,7 +573,7 @@ def generateSetpointEvent() { } else if (mode == "auxHeatOnly") { - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale) + sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint) } @@ -608,7 +608,7 @@ void raiseSetpoint() { targetvalue = maxCoolingSetpoint } - sendEvent("name":"thermostatSetpoint", "value":targetvalue, "unit":location.temperatureScale, displayed: false) + 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 @@ -644,7 +644,7 @@ void lowerSetpoint() { targetvalue = minCoolingSetpoint } - sendEvent("name":"thermostatSetpoint", "value":targetvalue, "unit":location.temperatureScale, displayed: false) + 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 @@ -690,10 +690,10 @@ void alterSetpoint(temp) { def coolingValue = location.temperatureScale == "C"? convertCtoF(targetCoolingSetpoint) : targetCoolingSetpoint def heatingValue = location.temperatureScale == "C"? convertCtoF(targetHeatingSetpoint) : targetHeatingSetpoint - if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) { + if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) { sendEvent("name": "thermostatSetpoint", "value": temp.value, displayed: false) - sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint, "unit": location.temperatureScale) - sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint, "unit": location.temperatureScale) + 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()" diff --git a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy index d8c2179..848b7b7 100644 --- a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy +++ b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy @@ -66,7 +66,7 @@ def authPage() { // get rid of next button until the user is actually auth'd if (!oauthTokenProvided) { return dynamicPage(name: "auth", title: "Login", nextPage: "", uninstall:uninstallAllowed) { - section() { + section(){ paragraph "Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button." href url:redirectUrl, style:"embedded", required:true, title:"ecobee", description:description } @@ -76,7 +76,7 @@ def authPage() { log.debug "thermostat list: $stats" log.debug "sensor list: ${sensorsDiscovered()}" return dynamicPage(name: "auth", title: "Select Your Thermostats", uninstall: true) { - section("") { + section(""){ paragraph "Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings." input(name: "thermostats", title:"", type: "enum", required:true, multiple:true, description: "Tap to choose", metadata:[values:stats]) } @@ -84,7 +84,7 @@ def authPage() { def options = sensorsDiscovered() ?: [] def numFound = options.size() ?: 0 if (numFound > 0) { - section("") { + section(""){ paragraph "Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings." input(name: "ecobeesensors", title:"Select Ecobee Sensors (${numFound} found)", type: "enum", required:false, description: "Tap to choose", multiple:true, options:options) } @@ -115,12 +115,13 @@ def callback() { def code = params.code def oauthState = params.state - if (oauthState == atomicState.oauthInitState) { + if (oauthState == atomicState.oauthInitState){ + def tokenParams = [ - grant_type: "authorization_code", - code : code, - client_id : smartThingsClientId, - redirect_uri: callbackUrl + grant_type: "authorization_code", + code : code, + client_id : smartThingsClientId, + redirect_uri: callbackUrl ] def tokenUrl = "https://www.ecobee.com/home/token?${toQueryString(tokenParams)}" @@ -128,6 +129,9 @@ def callback() { httpPost(uri: tokenUrl) { resp -> atomicState.refreshToken = resp.data.refresh_token atomicState.authToken = resp.data.access_token + log.debug "swapped token: $resp.data" + log.debug "atomicState.refreshToken: ${atomicState.refreshToken}" + log.debug "atomicState.authToken: ${atomicState.authToken}" } if (atomicState.authToken) { @@ -144,8 +148,8 @@ def callback() { def success() { def message = """ -

Your ecobee Account is now connected to SmartThings!

-

Click 'Done' to finish setup.

+

Your ecobee Account is now connected to SmartThings!

+

Click 'Done' to finish setup.

""" connectionStatus(message) } @@ -167,63 +171,64 @@ def connectionStatus(message, redirectUrl = null) { } def html = """ - - - - - Ecobee & SmartThings connection - - - -
+ + + + +Ecobee & SmartThings connection + + + +
ecobee icon connected device icon SmartThings logo ${message} -
- - - """ +
+ + +""" render contentType: 'text/html', data: html } @@ -232,26 +237,19 @@ def getEcobeeThermostats() { log.debug "getting device list" atomicState.remoteSensors = [] - def bodyParams = [ - selection: [ - selectionType: "registered", - selectionMatch: "", - includeRuntime: true, - includeSensors: true - ] - ] + def requestBody = '{"selection":{"selectionType":"registered","selectionMatch":"","includeRuntime":true,"includeSensors":true}}' + def deviceListParams = [ - uri: apiEndpoint, - path: "/1/thermostat", - headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], - // TODO - the query string below is not consistent with the Ecobee docs: - // https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml - query: [format: 'json', body: toJson(bodyParams)] + uri: apiEndpoint, + path: "/1/thermostat", + headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], + query: [format: 'json', body: requestBody] ] def stats = [:] try { httpGet(deviceListParams) { resp -> + if (resp.status == 200) { resp.data.thermostatList.each { stat -> atomicState.remoteSensors = atomicState.remoteSensors == null ? stat.remoteSensors : atomicState.remoteSensors << stat.remoteSensors @@ -291,10 +289,9 @@ Map sensorsDiscovered() { } def getThermostatDisplayName(stat) { - if(stat?.name) { - return stat.name.toString() - } - return (getThermostatTypeName(stat) + " (${stat.identifier})").toString() + if(stat?.name) + return stat.name.toString() + return (getThermostatTypeName(stat) + " (${stat.identifier})").toString() } def getThermostatTypeName(stat) { @@ -313,6 +310,7 @@ def updated() { } def initialize() { + log.debug "initialize" def devices = thermostats.collect { dni -> def d = getChildDevice(dni) @@ -352,6 +350,8 @@ def initialize() { log.warn "delete: ${delete}, deleting ${delete.size()} thermostats" delete.each { deleteChildDevice(it.deviceNetworkId) } //inherits from SmartApp (data-management) + atomicState.thermostatData = [:] //reset Map to store thermostat data + //send activity feeds to tell that device is connected def notificationMessage = "is connected to SmartThings" sendActivityFeeds(notificationMessage) @@ -381,41 +381,75 @@ def pollHandler() { } def pollChildren(child = null) { - def thermostatIdsString = getChildDeviceIdsString() - log.debug "polling children: $thermostatIdsString" - - def requestBody = [ - selection: [ - selectionType: "thermostats", - selectionMatch: thermostatIdsString, - includeExtendedRuntime: true, - includeSettings: true, - includeRuntime: true, - includeSensors: true - ] - ] + def thermostatIdsString = getChildDeviceIdsString() + log.debug "polling children: $thermostatIdsString" + def data = "" + def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeExtendedRuntime":"true","includeSettings":"true","includeRuntime":"true","includeSensors":true}}' def result = false def pollParams = [ - uri: apiEndpoint, - path: "/1/thermostat", - headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], - // TODO - the query string below is not consistent with the Ecobee docs: - // https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml - query: [format: 'json', body: toJson(requestBody)] - ] + uri: apiEndpoint, + path: "/1/thermostat", + headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], + query: [format: 'json', body: jsonRequestBody] + ] try{ httpGet(pollParams) { resp -> if(resp.status == 200) { - log.debug "poll results returned resp.data ${resp.data}" - atomicState.remoteSensors = resp.data.thermostatList.remoteSensors - updateSensorData() - storeThermostatData(resp.data.thermostatList) - result = true - log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}" - } + log.debug "poll results returned resp.data ${resp.data}" + atomicState.remoteSensors = resp.data.thermostatList.remoteSensors + atomicState.thermostatData = resp.data + updateSensorData() + atomicState.thermostats = resp.data.thermostatList.inject([:]) { collector, stat -> + def dni = [ app.id, stat.identifier ].join('.') + + log.debug "updating dni $dni" + + data = [ + coolMode: (stat.settings.coolStages > 0), + heatMode: (stat.settings.heatStages > 0), + deviceTemperatureUnit: stat.settings.useCelsius, + minHeatingSetpoint: (stat.settings.heatRangeLow / 10), + maxHeatingSetpoint: (stat.settings.heatRangeHigh / 10), + minCoolingSetpoint: (stat.settings.coolRangeLow / 10), + maxCoolingSetpoint: (stat.settings.coolRangeHigh / 10), + autoMode: stat.settings.autoHeatCoolFeatureEnabled, + auxHeatMode: (stat.settings.hasHeatPump) && (stat.settings.hasForcedAir || stat.settings.hasElectric || stat.settings.hasBoiler), + temperature: (stat.runtime.actualTemperature / 10), + heatingSetpoint: stat.runtime.desiredHeat / 10, + coolingSetpoint: stat.runtime.desiredCool / 10, + thermostatMode: stat.settings.hvacMode, + humidity: stat.runtime.actualHumidity, + thermostatFanMode: stat.runtime.desiredFanMode + ] + + if (location.temperatureScale == "F") + { + data["temperature"] = data["temperature"] ? Math.round(data["temperature"].toDouble()) : data["temperature"] + data["heatingSetpoint"] = data["heatingSetpoint"] ? Math.round(data["heatingSetpoint"].toDouble()) : data["heatingSetpoint"] + data["coolingSetpoint"] = data["coolingSetpoint"] ? Math.round(data["coolingSetpoint"].toDouble()) : data["coolingSetpoint"] + data["minHeatingSetpoint"] = data["minHeatingSetpoint"] ? Math.round(data["minHeatingSetpoint"].toDouble()) : data["minHeatingSetpoint"] + data["maxHeatingSetpoint"] = data["maxHeatingSetpoint"] ? Math.round(data["maxHeatingSetpoint"].toDouble()) : data["maxHeatingSetpoint"] + data["minCoolingSetpoint"] = data["minCoolingSetpoint"] ? Math.round(data["minCoolingSetpoint"].toDouble()) : data["minCoolingSetpoint"] + data["maxCoolingSetpoint"] = data["maxCoolingSetpoint"] ? Math.round(data["maxCoolingSetpoint"].toDouble()) : data["maxCoolingSetpoint"] + + } + + if (data?.deviceTemperatureUnit == false && location.temperatureScale == "F") { + data["deviceTemperatureUnit"] = "F" + + } else { + data["deviceTemperatureUnit"] = "C" + } + + collector[dni] = [data:data] + return collector + } + result = true + log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}" + } } } catch (groovyx.net.http.HttpResponseException e) { log.trace "Exception polling children: " + e.response.data.status @@ -429,12 +463,13 @@ def pollChildren(child = null) { } // Poll Child is invoked from the Child Device itself as part of the Poll Capability -def pollChild() { +def pollChild(){ + def devices = getChildDevices() - if (pollChildren()) { + if (pollChildren()){ devices.each { child -> - if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")) { + if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")){ if(atomicState.thermostats[child.device.deviceNetworkId] != null) { def tData = atomicState.thermostats[child.device.deviceNetworkId] log.info "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}" @@ -457,38 +492,36 @@ void poll() { } def availableModes(child) { + debugEvent ("atomicState.thermostats = ${atomicState.thermostats}") + debugEvent ("Child DNI = ${child.device.deviceNetworkId}") def tData = atomicState.thermostats[child.device.deviceNetworkId] debugEvent("Data = ${tData}") - if(!tData) { + if(!tData) + { log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" + return null } def modes = ["off"] - if (tData.data.heatMode) { - modes.add("heat") - } - if (tData.data.coolMode) { - modes.add("cool") - } - if (tData.data.autoMode) { - modes.add("auto") - } - if (tData.data.auxHeatMode) { - modes.add("auxHeatOnly") - } + if (tData.data.heatMode) modes.add("heat") + if (tData.data.coolMode) modes.add("cool") + if (tData.data.autoMode) modes.add("auto") + if (tData.data.auxHeatMode) modes.add("auxHeatOnly") + + modes - return modes } def currentMode(child) { debugEvent ("atomicState.Thermos = ${atomicState.thermostats}") + debugEvent ("Child DNI = ${child.device.deviceNetworkId}") def tData = atomicState.thermostats[child.device.deviceNetworkId] @@ -497,11 +530,14 @@ def currentMode(child) { if(!tData) { log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" + + return null } def mode = tData.data.thermostatMode - return mode + + mode } def updateSensorData() { @@ -522,12 +558,12 @@ def updateSensorData() { } } + } else if (it.type == "occupancy") { - if(it.value == "true") { - occupancy = "active" - } else { + if(it.value == "true") + occupancy = "active" + else occupancy = "inactive" - } } } def dni = "ecobee_sensor-"+ it?.id + "-" + it?.code @@ -546,7 +582,7 @@ def getChildDeviceIdsString() { } def toJson(Map m) { - return groovy.json.JsonOutput.toJson(m) + return new org.json.JSONObject(m).toString() } def toQueryString(Map m) { @@ -559,24 +595,54 @@ private refreshAuthToken() { if(!atomicState.refreshToken) { log.warn "Can not refresh OAuth token since there is no refreshToken stored" } else { + def refreshParams = [ - method: 'POST', - uri : apiEndpoint, - path : "/token", - query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId], + method: 'POST', + uri : apiEndpoint, + path : "/token", + query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId], ] + log.debug refreshParams + def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials." //changed to httpPost try { def jsonMap httpPost(refreshParams) { resp -> + if(resp.status == 200) { log.debug "Token refreshed...calling saved RestAction now!" + debugEvent("Token refreshed ... calling saved RestAction now!") - saveTokenAndResumeAction(resp.data) - } - } + + log.debug resp + + jsonMap = resp.data + + if(resp.data) { + + log.debug resp.data + debugEvent("Response = ${resp.data}") + + atomicState.refreshToken = resp?.data?.refresh_token + atomicState.authToken = resp?.data?.access_token + + debugEvent("Refresh Token = ${atomicState.refreshToken}") + debugEvent("OAUTH Token = ${atomicState.authToken}") + + if(atomicState.action && atomicState.action != "") { + log.debug "Executing next action: ${atomicState.action}" + + "${atomicState.action}"() + + atomicState.action = "" + } + + } + atomicState.action = "" + } + } } catch (groovyx.net.http.HttpResponseException e) { log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}" def reAttemptPeriod = 300 // in sec @@ -596,220 +662,118 @@ private refreshAuthToken() { } } -/** - * Saves the refresh and auth token from the passed-in JSON object, - * and invokes any previously executing action that did not complete due to - * an expired token. - * - * @param json - an object representing the parsed JSON response from Ecobee - */ -private void saveTokenAndResumeAction(json) { - log.debug "token response json: $json" - if (json) { - debugEvent("Response = $json") - atomicState.refreshToken = json?.refresh_token - atomicState.authToken = json?.access_token - if (atomicState.action) { - log.debug "got refresh token, executing next action: ${atomicState.action}" - "${atomicState.action}"() - } - } else { - log.warn "did not get response body from refresh token response" - } - atomicState.action = "" +def resumeProgram(child, deviceId) { + + + def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}]}' + def result = sendJson(jsonRequestBody) + return result } -/** - * Executes the resume program command on the Ecobee thermostat - * @param deviceId - the ID of the device - * - * @retrun true if the command was successful, false otherwise. - */ -boolean resumeProgram(deviceId) { - def payload = [ - selection: [ - selectionType: "thermostats", - selectionMatch: deviceId, - includeRuntime: true - ], - functions: [ - [ - type: "resumeProgram" - ] - ] - ] - return sendCommandToEcobee(payload) +def setHold(child, heating, cooling, deviceId, sendHoldType) { + + int h = heating * 10 + int c = cooling * 10 + def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": '+sendHoldType+' } } ]}' + + def result = sendJson(child, jsonRequestBody) + return result } -/** - * Executes the set hold command on the Ecobee thermostat - * @param heating - The heating temperature to set in fahrenheit - * @param cooling - the cooling temperature to set in fahrenheit - * @param deviceId - the ID of the device - * @param sendHoldType - the hold type to execute - * - * @return true if the command was successful, false otherwise - */ -boolean setHold(heating, cooling, deviceId, sendHoldType) { - // Ecobee requires that temp values be in fahrenheit multiplied by 10. - int h = heating * 10 - int c = cooling * 10 +def setFanMode(child, heating, cooling, deviceId, sendHoldType, fanMode) { - def payload = [ - selection: [ - selectionType: "thermostats", - selectionMatch: deviceId, - includeRuntime: true - ], - functions: [ - [ - type: "setHold", - params: [ - coolHoldTemp: c, - heatHoldTemp: h, - holdType: sendHoldType - ] - ] - ] - ] + int h = heating * 10 + int c = cooling * 10 - return sendCommandToEcobee(payload) + + def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": '+sendHoldType+', "fan": '+fanMode+' } } ]}' + def result = sendJson(child, jsonRequestBody) + return result } -/** - * Executes the set fan mode command on the Ecobee thermostat - * @param heating - The heating temperature to set in fahrenheit - * @param cooling - the cooling temperature to set in fahrenheit - * @param deviceId - the ID of the device - * @param sendHoldType - the hold type to execute - * @param fanMode - the fan mode to set to - * - * @return true if the command was successful, false otherwise - */ -boolean setFanMode(heating, cooling, deviceId, sendHoldType, fanMode) { - // Ecobee requires that temp values be in fahrenheit multiplied by 10. - int h = heating * 10 - int c = cooling * 10 +def setMode(child, mode, deviceId) { + def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"thermostat": {"settings":{"hvacMode":"'+"${mode}"+'"}}}' - def payload = [ - selection: [ - selectionType: "thermostats", - selectionMatch: deviceId, - includeRuntime: true - ], - functions: [ - [ - type: "setHold", - params: [ - coolHoldTemp: c, - heatHoldTemp: h, - holdType: sendHoldType, - fan: fanMode - ] - ] - ] - ] - - return sendCommandToEcobee(payload) + def result = sendJson(jsonRequestBody) + return result } -/** - * Sets the mode of the Ecobee thermostat - * @param mode - the mode to set to - * @param deviceId - the ID of the device - * - * @return true if the command was successful, false otherwise - */ -boolean setMode(mode, deviceId) { - def payload = [ - selection: [ - selectionType: "thermostats", - selectionMatch: deviceId, - includeRuntime: true - ], - thermostat: [ - settings: [ - hvacMode: mode - ] - ] - ] - return sendCommandToEcobee(payload) -} +def sendJson(child = null, String jsonBody) { -/** - * Makes a request to the Ecobee API to actuate the thermostat. - * Used by command methods to send commands to Ecobee. - * - * @param bodyParams - a map of request parameters to send to Ecobee. - * - * @return true if the command was accepted by Ecobee without error, false otherwise. - */ -private boolean sendCommandToEcobee(Map bodyParams) { - def isSuccess = false + def returnStatus = false def cmdParams = [ - uri: apiEndpoint, - path: "/1/thermostat", - headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"], - body: toJson(bodyParams) + uri: apiEndpoint, + path: "/1/thermostat", + headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"], + body: jsonBody ] try{ - httpPost(cmdParams) { resp -> - if(resp.status == 200) { - log.debug "updated ${resp.data}" - def returnStatus = resp.data.status.code - if (returnStatus == 0) { - log.debug "Successful call to ecobee API." - isSuccess = true - } else { - log.debug "Error return code = ${returnStatus}" - debugEvent("Error return code = ${returnStatus}") - } - } - } + httpPost(cmdParams) { resp -> + + if(resp.status == 200) { + + log.debug "updated ${resp.data}" + returnStatus = resp.data.status.code + if (resp.data.status.code == 0) + log.debug "Successful call to ecobee API." + else { + log.debug "Error return code = ${resp.data.status.code}" + debugEvent("Error return code = ${resp.data.status.code}") + } + } + } } catch (groovyx.net.http.HttpResponseException e) { log.trace "Exception Sending Json: " + e.response.data.status debugEvent ("sent Json & got http status ${e.statusCode} - ${e.response.data.status.code}") if (e.response.data.status.code == 14) { - // TODO - figure out why we're setting the next action to be pollChildren - // after refreshing auth token. Is it to keep UI in sync, or just copy/paste error? atomicState.action = "pollChildren" log.debug "Refreshing your auth_token!" refreshAuthToken() - } else { + } + else { debugEvent("Authentication error, invalid authentication method, lack of credentials, etc.") log.error "Authentication error, invalid authentication method, lack of credentials, etc." } } - return isSuccess + if (returnStatus == 0) + return true + else + return false } -def getChildName() { return "Ecobee Thermostat" } -def getSensorChildName() { return "Ecobee Sensor" } +def getChildName() { "Ecobee Thermostat" } +def getSensorChildName() { "Ecobee Sensor" } def getServerUrl() { return "https://graph.api.smartthings.com" } def getShardUrl() { return getApiServerUrl() } -def getCallbackUrl() { return "https://graph.api.smartthings.com/oauth/callback" } -def getBuildRedirectUrl() { return "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${shardUrl}" } -def getApiEndpoint() { return "https://api.ecobee.com" } -def getSmartThingsClientId() { return appSettings.clientId } +def getCallbackUrl() { "https://graph.api.smartthings.com/oauth/callback" } +def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${shardUrl}" } +def getApiEndpoint() { "https://api.ecobee.com" } +def getSmartThingsClientId() { appSettings.clientId } def debugEvent(message, displayEvent = false) { + def results = [ - name: "appdebug", - descriptionText: message, - displayed: displayEvent + name: "appdebug", + descriptionText: message, + displayed: displayEvent ] log.debug "Generating AppDebug Event: ${results}" sendEvent (results) + +} + +def debugEventFromParent(child, message) { + if (child != null) { child.sendEvent("name":"debugEventFromParent", "value":message, "description":message, displayed: true, isStateChange: true)} } //send both push notification and mobile activity feeds -def sendPushAndFeeds(notificationMessage) { +def sendPushAndFeeds(notificationMessage){ log.warn "sendPushAndFeeds >> notificationMessage: ${notificationMessage}" log.warn "sendPushAndFeeds >> atomicState.timeSendPush: ${atomicState.timeSendPush}" - if (atomicState.timeSendPush) { - if (now() - atomicState.timeSendPush > 86400000) { // notification is sent to remind user once a day + if (atomicState.timeSendPush){ + if (now() - atomicState.timeSendPush > 86400000){ // notification is sent to remind user once a day sendPush("Your Ecobee thermostat " + notificationMessage) sendActivityFeeds(notificationMessage) atomicState.timeSendPush = now() @@ -822,58 +786,6 @@ def sendPushAndFeeds(notificationMessage) { atomicState.authToken = null } -/** - * Stores data about the thermostats in atomicState. - * @param thermostats - a list of thermostats as returned from the Ecobee API - */ -private void storeThermostatData(thermostats) { - log.trace "Storing thermostat data: $thermostats" - def data - atomicState.thermostats = thermostats.inject([:]) { collector, stat -> - def dni = [ app.id, stat.identifier ].join('.') - log.debug "updating dni $dni" - - data = [ - coolMode: (stat.settings.coolStages > 0), - heatMode: (stat.settings.heatStages > 0), - deviceTemperatureUnit: stat.settings.useCelsius, - minHeatingSetpoint: (stat.settings.heatRangeLow / 10), - maxHeatingSetpoint: (stat.settings.heatRangeHigh / 10), - minCoolingSetpoint: (stat.settings.coolRangeLow / 10), - maxCoolingSetpoint: (stat.settings.coolRangeHigh / 10), - autoMode: stat.settings.autoHeatCoolFeatureEnabled, - auxHeatMode: (stat.settings.hasHeatPump) && (stat.settings.hasForcedAir || stat.settings.hasElectric || stat.settings.hasBoiler), - temperature: (stat.runtime.actualTemperature / 10), - heatingSetpoint: stat.runtime.desiredHeat / 10, - coolingSetpoint: stat.runtime.desiredCool / 10, - thermostatMode: stat.settings.hvacMode, - humidity: stat.runtime.actualHumidity, - thermostatFanMode: stat.runtime.desiredFanMode - ] - if (location.temperatureScale == "F") { - data["temperature"] = data["temperature"] ? Math.round(data["temperature"].toDouble()) : data["temperature"] - data["heatingSetpoint"] = data["heatingSetpoint"] ? Math.round(data["heatingSetpoint"].toDouble()) : data["heatingSetpoint"] - data["coolingSetpoint"] = data["coolingSetpoint"] ? Math.round(data["coolingSetpoint"].toDouble()) : data["coolingSetpoint"] - data["minHeatingSetpoint"] = data["minHeatingSetpoint"] ? Math.round(data["minHeatingSetpoint"].toDouble()) : data["minHeatingSetpoint"] - data["maxHeatingSetpoint"] = data["maxHeatingSetpoint"] ? Math.round(data["maxHeatingSetpoint"].toDouble()) : data["maxHeatingSetpoint"] - data["minCoolingSetpoint"] = data["minCoolingSetpoint"] ? Math.round(data["minCoolingSetpoint"].toDouble()) : data["minCoolingSetpoint"] - data["maxCoolingSetpoint"] = data["maxCoolingSetpoint"] ? Math.round(data["maxCoolingSetpoint"].toDouble()) : data["maxCoolingSetpoint"] - - } - - if (data?.deviceTemperatureUnit == false && location.temperatureScale == "F") { - data["deviceTemperatureUnit"] = "F" - - } else { - data["deviceTemperatureUnit"] = "C" - } - - collector[dni] = [data:data] - return collector - } - log.debug "updated ${atomicState.thermostats?.size()} thermostats: ${atomicState.thermostats}" -} - def sendActivityFeeds(notificationMessage) { def devices = getChildDevices() devices.each { child -> @@ -881,6 +793,14 @@ def sendActivityFeeds(notificationMessage) { } } +def roundC (tempC) { + return String.format("%.1f", (Math.round(tempC * 2))/2) +} + def convertFtoC (tempF) { return String.format("%.1f", (Math.round(((tempF - 32)*(5/9)) * 2))/2) } + +def convertCtoF (tempC) { + return (Math.round(tempC * (9/5)) + 32).toInteger() +} From 5c2e06c98d80493a39f69f8a95d11715c5eef939 Mon Sep 17 00:00:00 2001 From: Lars Finander Date: Tue, 6 Sep 2016 10:43:03 -0600 Subject: [PATCH 16/18] DVCSMP-2004 Philips Hue: Nullpointer in bridge --- devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy | 8 ++++---- smartapps/smartthings/hue-connect.src/hue-connect.groovy | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy b/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy index 0986ae8..18e9a25 100644 --- a/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy +++ b/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy @@ -62,7 +62,7 @@ def parse(description) { log.trace "HUE BRIDGE, GENERATING EVENT: $map.name: $map.value" results << createEvent(name: "${map.name}", value: "${map.value}") } else { - log.trace "Parsing description" + log.trace "Parsing description" def msg = parseLanMessage(description) if (msg.body) { def contentType = msg.headers["Content-Type"] @@ -72,13 +72,13 @@ def parse(description) { log.info "Bridge response: $msg.body" } else { // Sending Bulbs List to parent" - if (parent.state.inBulbDiscovery) - log.info parent.bulbListHandler(device.hub.id, msg.body) + if (parent.isInBulbDiscovery()) + log.info parent.bulbListHandler(device.hub.id, msg.body) } } else if (contentType?.contains("xml")) { log.debug "HUE BRIDGE ALREADY PRESENT" - parent.hubVerification(device.hub.id, msg.body) + parent.hubVerification(device.hub.id, msg.body) } } } diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index 7a6e8a3..fdfb509 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -757,6 +757,10 @@ def isValidSource(macAddress) { return (vbridges?.find {"${it.value.mac}" == macAddress}) != null } +def isInBulbDiscovery() { + return state.inBulbDiscovery +} + ///////////////////////////////////// //CHILD DEVICE METHODS ///////////////////////////////////// From d1aee1e87488c9a62d4a89702f008f5fdd476024 Mon Sep 17 00:00:00 2001 From: Lars Finander Date: Tue, 6 Sep 2016 12:42:11 -0600 Subject: [PATCH 17/18] Revert "WWST-40 Philips Hue: Implement device watch" This reverts commit bacd335991fcef93b0df24acae3a36510bdd427b. --- .../hue-bloom.src/hue-bloom.groovy | 9 -- .../smartthings/hue-bulb.src/hue-bulb.groovy | 9 -- .../hue-lux-bulb.src/hue-lux-bulb.groovy | 9 -- .../hue-white-ambiance-bulb.groovy | 9 -- .../hue-connect.src/hue-connect.groovy | 141 ++++++------------ 5 files changed, 47 insertions(+), 130 deletions(-) diff --git a/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy b/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy index 9f7d804..f99f436 100644 --- a/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy +++ b/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy @@ -16,7 +16,6 @@ metadata { capability "Switch" capability "Refresh" capability "Sensor" - capability "Health Check" command "setAdjustedColor" command "reset" @@ -56,10 +55,6 @@ metadata { } } -void installed() { - sendEvent(name: "checkInterval", value: 60 * 30, data: [protocol: "lan"], displayed: false) -} - // parse events into attributes def parse(description) { log.debug "parse() - $description" @@ -171,7 +166,3 @@ def verifyPercent(percent) { return false } } - -def ping() { - log.debug "${parent.ping(this)}" -} diff --git a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy index 1f63b09..0c3d917 100644 --- a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy +++ b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy @@ -17,7 +17,6 @@ metadata { capability "Switch" capability "Refresh" capability "Sensor" - capability "Health Check" command "setAdjustedColor" command "reset" @@ -65,10 +64,6 @@ metadata { } } -void installed() { - sendEvent(name: "checkInterval", value: 60 * 30, data: [protocol: "lan"], displayed: false) -} - // parse events into attributes def parse(description) { log.debug "parse() - $description" @@ -187,7 +182,3 @@ def verifyPercent(percent) { return false } } - -def ping() { - log.trace "${parent.ping(this)}" -} diff --git a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy index b1f076d..728b265 100644 --- a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy +++ b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy @@ -14,7 +14,6 @@ metadata { capability "Switch" capability "Refresh" capability "Sensor" - capability "Health Check" command "refresh" } @@ -49,10 +48,6 @@ metadata { } } -void installed() { - sendEvent(name: "checkInterval", value: 60 * 30, data: [protocol: "lan"], displayed: false) -} - // parse events into attributes def parse(description) { log.debug "parse() - $description" @@ -92,7 +87,3 @@ void refresh() { log.debug "Executing 'refresh'" parent.manualRefresh() } - -def ping() { - log.debug "${parent.ping(this)}" -} diff --git a/devicetypes/smartthings/hue-white-ambiance-bulb.src/hue-white-ambiance-bulb.groovy b/devicetypes/smartthings/hue-white-ambiance-bulb.src/hue-white-ambiance-bulb.groovy index fd8c8bf..0a25741 100644 --- a/devicetypes/smartthings/hue-white-ambiance-bulb.src/hue-white-ambiance-bulb.groovy +++ b/devicetypes/smartthings/hue-white-ambiance-bulb.src/hue-white-ambiance-bulb.groovy @@ -15,7 +15,6 @@ metadata { capability "Color Temperature" capability "Switch" capability "Refresh" - capability "Health Check" command "refresh" } @@ -54,10 +53,6 @@ metadata { } } -void installed() { - sendEvent(name: "checkInterval", value: 60 * 30, data: [protocol: "lan"], displayed: false) -} - // parse events into attributes def parse(description) { log.debug "parse() - $description" @@ -106,7 +101,3 @@ void refresh() { log.debug "Executing 'refresh'" parent.manualRefresh() } - -def ping() { - log.debug "${parent.ping(this)}" -} diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index fdfb509..9ca7773 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -95,7 +95,8 @@ def bridgeDiscoveryFailed() { } } -def bridgeLinking() { +def bridgeLinking() +{ int linkRefreshcount = !state.linkRefreshcount ? 0 : state.linkRefreshcount as int state.linkRefreshcount = linkRefreshcount + 1 def refreshInterval = 3 @@ -327,7 +328,7 @@ def bulbListHandler(hub, data = "") { def object = new groovy.json.JsonSlurper().parseText(data) object.each { k,v -> if (v instanceof Map) - bulbs[k] = [id: k, name: v.name, type: v.type, modelid: v.modelid, hub:hub, online: v.state?.reachable] + bulbs[k] = [id: k, name: v.name, type: v.type, modelid: v.modelid, hub:hub] } } def bridge = null @@ -447,6 +448,7 @@ def addBridge() { updateBridgeStatus(childDevice) childDevice.sendEvent(name: "idNumber", value: idNumber) + if (vbridge.value.ip && vbridge.value.port) { if (vbridge.value.ip.contains(".")) { childDevice.sendEvent(name: "networkAddress", value: vbridge.value.ip + ":" + vbridge.value.port) @@ -647,7 +649,8 @@ def locationHandler(evt) { } } } - } else if (parsedEvent.headers && parsedEvent.body) { + } + else if (parsedEvent.headers && parsedEvent.body) { log.trace "HUE BRIDGE RESPONSES" def headerString = parsedEvent.headers.toString() if (headerString?.contains("xml")) { @@ -730,7 +733,7 @@ private void updateBridgeStatus(childDevice) { private void checkBridgeStatus() { def bridges = getHueBridges() // Check if each bridge has been heard from within the last 16 minutes (3 poll intervals times 5 minutes plus buffer) - def time = now() - (1000 * 60 * 30) + def time = now() - (1000 * 60 * 16) bridges.each { def d = getChildDevice(it.value.mac) if(d) { @@ -743,8 +746,6 @@ private void checkBridgeStatus() { if (it.value.lastActivity < time) { // it.value.lastActivity != null && log.warn "Bridge $it.key is Offline" d.sendEvent(name: "status", value: "Offline") - // set all lights to offline since bridge is not reachable - state.bulbs?.each {it.value.online = false} } else { d.sendEvent(name: "status", value: "Online")//setOnline(false) } @@ -784,7 +785,8 @@ def parse(childDevice, description) { if (body instanceof java.util.Map) { // get (poll) reponse return handlePoll(body) - } else { + } + else { //put response return handleCommandResponse(body) } @@ -881,40 +883,36 @@ private handleCommandResponse(body) { // scan entire response before sending events to make sure they are always in the same order def updates = [:] - body.each { payload -> - log.debug $payload + body.each { payload -> + log.debug $payload if (payload?.success) { - def childDeviceNetworkId = app.id + "/" - def eventType + def childDeviceNetworkId = app.id + "/" + def eventType payload.success.each { k, v -> def data = k.split("/") if (data.length == 5) { childDeviceNetworkId = app.id + "/" + k.split("/")[2] if (!updates[childDeviceNetworkId]) updates[childDeviceNetworkId] = [:] - eventType = k.split("/")[4] + eventType = k.split("/")[4] updates[childDeviceNetworkId]."$eventType" = v } } } else if (payload.error) { log.warn "Error returned from Hue bridge error = ${body?.error}" - } - } + } + } // send events for each update found above (order of events should be same as handlePoll()) updates.each { childDeviceNetworkId, params -> def device = getChildDevice(childDeviceNetworkId) - def id = getId(device) - // If device is offline, then don't send events which will update device watch - if (isOnline(id)) { - sendBasicEvents(device, "on", params.on) - sendBasicEvents(device, "bri", params.bri) - sendColorEvents(device, params.xy, params.hue, params.sat, params.ct) - } - } + sendBasicEvents(device, "on", params.on) + sendBasicEvents(device, "bri", params.bri) + sendColorEvents(device, params.xy, params.hue, params.sat, params.ct) + } return [] -} + } /** * Handles a response to a poll (GET) sent to the Hue Bridge. @@ -934,32 +932,26 @@ private handleCommandResponse(body) { * @return empty array */ private handlePoll(body) { + if (state.updating) { + // If user just executed commands, then ignore poll to not confuse the turning on/off state + return [] + } + def bulbs = getChildDevices() for (bulb in body) { def device = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"} if (device) { if (bulb.value.state?.reachable) { - if (state.bulbs[bulb.key]?.online == false) { - // light just came back online, notify device watch - def lastActivity = now() - device.sendEvent(name: "deviceWatch-status", value: "ONLINE", description: "Last Activity is on ${new Date((long) lastActivity)}", displayed: false, isStateChange: true) - } - state.bulbs[bulb.key]?.online = true - - // If user just executed commands, then do not send events to avoid confusing the turning on/off state - if (!state.updating) { - sendBasicEvents(device, "on", bulb.value?.state?.on) - sendBasicEvents(device, "bri", bulb.value?.state?.bri) - sendColorEvents(device, bulb.value?.state?.xy, bulb.value?.state?.hue, bulb.value?.state?.sat, bulb.value?.state?.ct, bulb.value?.state?.colormode) - } + sendBasicEvents(device, "on", bulb.value?.state?.on) + sendBasicEvents(device, "bri", bulb.value?.state?.bri) + sendColorEvents(device, bulb.value?.state?.xy, bulb.value?.state?.hue, bulb.value?.state?.sat, bulb.value?.state?.ct, bulb.value?.state?.colormode) } else { - state.bulbs[bulb.key]?.online = false log.warn "$device is not reachable by Hue bridge" - } - } + } + } + } + return [] } - return [] -} private updateInProgress() { state.updating = true @@ -988,34 +980,22 @@ def hubVerification(bodytext) { def on(childDevice) { log.debug "Executing 'on'" - def id = getId(childDevice) - if (!isOnline(id)) { - return "Bulb is unreachable" - } updateInProgress() createSwitchEvent(childDevice, "on") - put("lights/$id/state", [on: true]) + put("lights/${getId(childDevice)}/state", [on: true]) return "Bulb is turning On" } def off(childDevice) { log.debug "Executing 'off'" - def id = getId(childDevice) - if (!isOnline(id)) { - return "Bulb is unreachable" - } updateInProgress() createSwitchEvent(childDevice, "off") - put("lights/$id/state", [on: false]) + put("lights/${getId(childDevice)}/state", [on: false]) return "Bulb is turning Off" } def setLevel(childDevice, percent) { log.debug "Executing 'setLevel'" - def id = getId(childDevice) - if (!isOnline(id)) { - return "Bulb is unreachable" - } updateInProgress() // 1 - 254 def level @@ -1030,64 +1010,48 @@ def setLevel(childDevice, percent) { // that means that the light will still be on when on is called next time // Lets emulate that here if (percent > 0) { - put("lights/$id/state", [bri: level, on: true]) + put("lights/${getId(childDevice)}/state", [bri: level, on: true]) } else { - put("lights/$id/state", [on: false]) + put("lights/${getId(childDevice)}/state", [on: false]) } return "Setting level to $percent" } def setSaturation(childDevice, percent) { log.debug "Executing 'setSaturation($percent)'" - def id = getId(childDevice) - if (!isOnline(id)) { - return "Bulb is unreachable" - } - - updateInProgress() + updateInProgress() // 0 - 254 def level = Math.min(Math.round(percent * 254 / 100), 254) // TODO should this be done by app only or should we default to on? createSwitchEvent(childDevice, "on") - put("lights/$id/state", [sat: level, on: true]) + put("lights/${getId(childDevice)}/state", [sat: level, on: true]) return "Setting saturation to $percent" } def setHue(childDevice, percent) { log.debug "Executing 'setHue($percent)'" - def id = getId(childDevice) - if (!isOnline(id)) { - return "Bulb is unreachable" - } updateInProgress() // 0 - 65535 def level = Math.min(Math.round(percent * 65535 / 100), 65535) // TODO should this be done by app only or should we default to on? createSwitchEvent(childDevice, "on") - put("lights/$id/state", [hue: level, on: true]) + put("lights/${getId(childDevice)}/state", [hue: level, on: true]) return "Setting hue to $percent" } def setColorTemperature(childDevice, huesettings) { log.debug "Executing 'setColorTemperature($huesettings)'" - def id = getId(childDevice) - if (!isOnline(id)) { - return "Bulb is unreachable" - } updateInProgress() // 153 (6500K) to 500 (2000K) def ct = hueSettings == 6500 ? 153 : Math.round(1000000/huesettings) createSwitchEvent(childDevice, "on") - put("lights/$id/state", [ct: ct, on: true]) + put("lights/${getId(childDevice)}/state", [ct: ct, on: true]) return "Setting color temperature to $percent" } def setColor(childDevice, huesettings) { log.debug "Executing 'setColor($huesettings)'" - def id = getId(childDevice) - if (!isOnline(id)) { - return "Bulb is unreachable" - } + updateInProgress() def value = [:] @@ -1144,23 +1108,15 @@ def setColor(childDevice, huesettings) { value.on = false createSwitchEvent(childDevice, value.on ? "on" : "off") - put("lights/$id/state", value) + put("lights/${getId(childDevice)}/state", value) return "Setting color to $value" } -def ping(childDevice) { - if (isOnline(getId(childDevice))) { - childDevice.sendEvent(name: "deviceWatch-ping", value: "ONLINE", description: "Hue Light is reachable", displayed: false, isStateChange: true) - return "Device is Online" - } else { - return "Device is Offline" - } -} - private getId(childDevice) { if (childDevice.device?.deviceNetworkId?.startsWith("HUE")) { return childDevice.device?.deviceNetworkId[3..-1] - } else { + } + else { return childDevice.device?.deviceNetworkId.split("/")[-1] } } @@ -1171,11 +1127,8 @@ private poll() { log.debug "GET: $host$uri" sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1 HOST: ${host} -""", physicalgraph.device.Protocol.LAN, selectedHue)) -} -private isOnline(id) { - return (state.bulbs[id].online != null && state.bulbs[id].online) || state.bulbs[id].online == null +""", physicalgraph.device.Protocol.LAN, selectedHue)) } private put(path, body) { @@ -1245,7 +1198,7 @@ def convertBulbListToMap() { if (state.bulbs instanceof java.util.List) { def map = [:] state.bulbs.unique {it.id}.each { bulb -> - map << ["${bulb.id}":["id":bulb.id, "name":bulb.name, "type": bulb.type, "modelid": bulb.modelid, "hub":bulb.hub, "online": bulb.online]] + map << ["${bulb.id}":["id":bulb.id, "name":bulb.name, "type": bulb.type, "modelid": bulb.modelid, "hub":bulb.hub]] } state.bulbs = map } From f3f5cc42c906f8add13f599a7c6580cf1fabab2a Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Tue, 6 Sep 2016 17:38:45 -0400 Subject: [PATCH 18/18] CREX-5826 - Missing braces (#1198) --- .../smartthings/notify-me-when.src/notify-me-when.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 1d8a055..3b286a8 100644 --- a/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy +++ b/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy @@ -116,11 +116,11 @@ private sendMessage(evt) { if (pushAndPhone != 'No') { log.debug 'Sending push and SMS' options.method = 'both' - else { + } else { log.debug 'Sending SMS' options.method = 'phone' } - else if (pushAndPhone != 'No') { + } else if (pushAndPhone != 'No') { log.debug 'Sending push' options.method = 'push' } else {