diff --git a/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy b/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy index e984bb6..2753e9b 100644 --- a/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy +++ b/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy @@ -22,10 +22,6 @@ metadata { capability "Polling" } - simulator { - // TODO: define status and reply messages here - } - tiles { valueTile("temperature", "device.temperature", width: 2, height: 2) { state("temperature", label:'${currentValue}°', unit:"F", @@ -56,16 +52,12 @@ metadata { } def refresh() { - log.debug "refresh..." + log.debug "refresh called" poll() } void poll() { log.debug "Executing 'poll' using parent SmartApp" - parent.pollChildren(this) -} + parent.pollChild(this) -//generate custom mobile activity feeds event -def generateActivityFeedsEvent(notificationMessage) { - sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true) } diff --git a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy index b439a63..7c718b3 100644 --- a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy +++ b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy @@ -19,34 +19,39 @@ metadata { definition (name: "Ecobee Thermostat", namespace: "smartthings", author: "SmartThings") { capability "Actuator" capability "Thermostat" + capability "Temperature Measurement" capability "Polling" capability "Sensor" - capability "Refresh" + capability "Refresh" + capability "Relative Humidity Measurement" - command "generateEvent" - command "raiseSetpoint" - command "lowerSetpoint" - command "resumeProgram" - command "switchMode" + command "generateEvent" + command "raiseSetpoint" + command "lowerSetpoint" + command "resumeProgram" + command "switchMode" - attribute "thermostatSetpoint","number" - attribute "thermostatStatus","string" + attribute "thermostatSetpoint","number" + attribute "thermostatStatus","string" + attribute "maxHeatingSetpoint", "number" + attribute "minHeatingSetpoint", "number" + attribute "maxCoolingSetpoint", "number" + attribute "minCoolingSetpoint", "number" + attribute "deviceTemperatureUnit", "number" } - simulator { } - - tiles { + tiles { valueTile("temperature", "device.temperature", width: 2, height: 2) { state("temperature", label:'${currentValue}°', unit:"F", - backgroundColors:[ - [value: 31, color: "#153591"], - [value: 44, color: "#1e9cbb"], - [value: 59, color: "#90d2a7"], - [value: 74, color: "#44b621"], - [value: 84, color: "#f1d801"], - [value: 95, color: "#d04e00"], - [value: 96, color: "#bc2323"] - ] + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] ) } standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { @@ -94,8 +99,11 @@ metadata { state "resume", action:"resumeProgram", nextState: "updating", label:'Resume Schedule', icon:"st.samsung.da.oven_ic_send" state "updating", label:"Working", icon: "st.secondary.secondary" } + valueTile("humidity", "device.humidity", decoration: "flat") { + state "humidity", label:'${currentValue}% humidity' + } main "temperature" - details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh"]) + details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh", "humidity"]) } preferences { @@ -107,8 +115,6 @@ metadata { // parse events into attributes def parse(String description) { log.debug "Parsing '${description}'" - // TODO: handle '' attribute - } def refresh() { @@ -133,16 +139,24 @@ def generateEvent(Map results) { def isChange = false def isDisplayed = true def event = [name: name, linkText: linkText, descriptionText: getThermostatDescriptionText(name, value, linkText), - handlerName: name] + handlerName: name] - if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") { - def sendValue = value? convertTemperatureIfNeeded(value.toDouble(), "F", 1): value //API return temperature value in F + if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint" ) { + def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F + sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue isChange = isTemperatureStateChange(device, name, value.toString()) isDisplayed = isChange event << [value: sendValue, isStateChange: isChange, displayed: isDisplayed] - } else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){ + } else if (name=="maxCoolingSetpoint" || name=="minCoolingSetpoint" || name=="maxHeatingSetpoint" || name=="minHeatingSetpoint") { + def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F + sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue + event << [value: sendValue, displayed: false] + } else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){ isChange = isStateChange(device, name, value.toString()) event << [value: value.toString(), isStateChange: isChange, displayed: false] + } else if (name=="humidity") { + isChange = isStateChange(device, name, value.toString()) + event << [value: value.toString(), isStateChange: isChange, displayed: false, unit: "%"] } else { isChange = isStateChange(device, name, value.toString()) isDisplayed = isChange @@ -158,13 +172,19 @@ def generateEvent(Map results) { //return descriptionText to be shown on mobile activity feed private getThermostatDescriptionText(name, value, linkText) { if(name == "temperature") { - return "$linkText temperature is $value°F" + def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F + sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue + return "$linkText temperature is $sendValue ${location.temperatureScale}" } else if(name == "heatingSetpoint") { - return "heating setpoint is $value°F" + def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F + sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue + return "heating setpoint is $sendValue ${location.temperatureScale}" } else if(name == "coolingSetpoint"){ - return "cooling setpoint is $value°F" + def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F + sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue + return "cooling setpoint is $sendValue ${location.temperatureScale}" } else if (name == "thermostatMode") { return "thermostat mode is ${value}" @@ -172,26 +192,26 @@ private getThermostatDescriptionText(name, value, linkText) { } else if (name == "thermostatFanMode") { return "thermostat fan mode is ${value}" + } else if (name == "humidity") { + return "humidity is ${value} %" } else { return "${name} = ${value}" } } void setHeatingSetpoint(setpoint) { - setHeatingSetpoint(setpoint.toDouble()) -} - -void setHeatingSetpoint(Double setpoint) { -// def mode = device.currentValue("thermostatMode") - def heatingSetpoint = setpoint + log.debug "***heating setpoint $setpoint" + def heatingSetpoint = setpoint.toDouble() def coolingSetpoint = device.currentValue("coolingSetpoint").toDouble() def deviceId = device.deviceNetworkId.split(/\./).last() + def maxHeatingSetpoint = device.currentValue("maxHeatingSetpoint").toDouble() + def minHeatingSetpoint = device.currentValue("minHeatingSetpoint").toDouble() //enforce limits of heatingSetpoint - if (heatingSetpoint > 79) { - heatingSetpoint = 79 - } else if (heatingSetpoint < 45) { - heatingSetpoint = 45 + if (heatingSetpoint > maxHeatingSetpoint) { + heatingSetpoint = maxHeatingSetpoint + } else if (heatingSetpoint < minHeatingSetpoint) { + heatingSetpoint = minHeatingSetpoint } //enforce limits of heatingSetpoint vs coolingSetpoint @@ -201,32 +221,34 @@ void setHeatingSetpoint(Double setpoint) { log.debug "Sending setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}" + def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint + def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint + def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite" - if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId, sendHoldType)) { + if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) { sendEvent("name":"heatingSetpoint", "value":heatingSetpoint) sendEvent("name":"coolingSetpoint", "value":coolingSetpoint) log.debug "Done setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}" generateSetpointEvent() generateStatusEvent() } else { - log.error "Error setHeatingSetpoint(setpoint)" //This error is handled by the connect app + log.error "Error setHeatingSetpoint(setpoint)" } } void setCoolingSetpoint(setpoint) { - setCoolingSetpoint(setpoint.toDouble()) -} - -void setCoolingSetpoint(Double setpoint) { -// def mode = device.currentValue("thermostatMode") + log.debug "***cooling setpoint $setpoint" def heatingSetpoint = device.currentValue("heatingSetpoint").toDouble() - def coolingSetpoint = setpoint + def coolingSetpoint = setpoint.toDouble() def deviceId = device.deviceNetworkId.split(/\./).last() + def maxCoolingSetpoint = device.currentValue("maxCoolingSetpoint").toDouble() + def minCoolingSetpoint = device.currentValue("minCoolingSetpoint").toDouble() - if (coolingSetpoint > 92) { - coolingSetpoint = 92 - } else if (coolingSetpoint < 65) { - coolingSetpoint = 65 + + if (coolingSetpoint > maxCoolingSetpoint) { + coolingSetpoint = maxCoolingSetpoint + } else if (coolingSetpoint < minCoolingSetpoint) { + coolingSetpoint = minCoolingSetpoint } //enforce limits of heatingSetpoint vs coolingSetpoint @@ -236,15 +258,18 @@ void setCoolingSetpoint(Double setpoint) { log.debug "Sending setCoolingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}" + def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint + def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint + def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite" - if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId, sendHoldType)) { + if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) { sendEvent("name":"heatingSetpoint", "value":heatingSetpoint) sendEvent("name":"coolingSetpoint", "value":coolingSetpoint) log.debug "Done setCoolingSetpoint>> coolingSetpoint = ${coolingSetpoint}, heatingSetpoint = ${heatingSetpoint}" generateSetpointEvent() generateStatusEvent() } else { - log.error "Error setCoolingSetpoint(setpoint)" //This error is handled by the connect app + log.error "Error setCoolingSetpoint(setpoint)" } } @@ -448,25 +473,21 @@ def auto() { def fanOn() { log.debug "fanOn" // parent.setFanMode (this,"on") - } def fanAuto() { log.debug "fanAuto" // parent.setFanMode (this,"auto") - } def fanCirculate() { log.debug "fanCirculate" // parent.setFanMode (this,"circulate") - } def fanOff() { log.debug "fanOff" // parent.setFanMode (this,"off") - } def generateSetpointEvent() { @@ -476,20 +497,41 @@ def generateSetpointEvent() { def mode = device.currentValue("thermostatMode") log.debug "Current Mode = ${mode}" - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() + def heatingSetpoint = device.currentValue("heatingSetpoint") log.debug "Heating Setpoint = ${heatingSetpoint}" - def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() + def coolingSetpoint = device.currentValue("coolingSetpoint") log.debug "Cooling Setpoint = ${coolingSetpoint}" + def maxHeatingSetpoint = device.currentValue("maxHeatingSetpoint") + def maxCoolingSetpoint = device.currentValue("maxCoolingSetpoint") + def minHeatingSetpoint = device.currentValue("minHeatingSetpoint") + def minCoolingSetpoint = device.currentValue("minCoolingSetpoint") + + if(location.temperatureScale == "C") + { + maxHeatingSetpoint = roundC(maxHeatingSetpoint) + maxCoolingSetpoint = roundC(maxCoolingSetpoint) + minHeatingSetpoint = roundC(minHeatingSetpoint) + minCoolingSetpoint = roundC(minCoolingSetpoint) + heatingSetpoint = roundC(heatingSetpoint) + coolingSetpoint = roundC(coolingSetpoint) + } + + sendEvent("name":"maxHeatingSetpoint", "value":maxHeatingSetpoint, "unit":location.temperatureScale) + sendEvent("name":"maxCoolingSetpoint", "value":maxCoolingSetpoint, "unit":location.temperatureScale) + sendEvent("name":"minHeatingSetpoint", "value":minHeatingSetpoint, "unit":location.temperatureScale) + sendEvent("name":"minCoolingSetpoint", "value":minCoolingSetpoint, "unit":location.temperatureScale) + + if (mode == "heat") { - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()) + sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint ) } else if (mode == "cool") { - sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()) + sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint) } else if (mode == "auto") { @@ -499,9 +541,9 @@ def generateSetpointEvent() { sendEvent("name":"thermostatSetpoint", "value":"Off") - } else if (mode == "emergencyHeat") { + } else if (mode == "auxHeatOnly") { - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()) + sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint) } @@ -510,26 +552,30 @@ def generateSetpointEvent() { void raiseSetpoint() { def mode = device.currentValue("thermostatMode") def targetvalue + def maxHeatingSetpoint = device.currentValue("maxHeatingSetpoint") + def maxCoolingSetpoint = device.currentValue("maxCoolingSetpoint") + if (mode == "off" || mode == "auto") { log.warn "this mode: $mode does not allow raiseSetpoint" } else { - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() - def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() - def thermostatSetpoint = device.currentValue("thermostatSetpoint").toInteger() + def heatingSetpoint = device.currentValue("heatingSetpoint") + def coolingSetpoint = device.currentValue("coolingSetpoint") + def thermostatSetpoint = device.currentValue("thermostatSetpoint") log.debug "raiseSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}" if (device.latestState('thermostatSetpoint')) { - targetvalue = device.latestState('thermostatSetpoint').value as Integer + targetvalue = device.latestState('thermostatSetpoint').value + targetvalue = location.temperatureScale == "F"? targetvalue.toInteger() : targetvalue.toDouble() } else { targetvalue = 0 } - targetvalue = targetvalue + 1 + targetvalue = location.temperatureScale == "F"? targetvalue + 1 : targetvalue + 0.5 - if (mode == "heat" && targetvalue > 79) { - targetvalue = 79 - } else if (mode == "cool" && targetvalue > 92) { - targetvalue = 92 + if ((mode == "heat" || mode == "auxHeatOnly") && targetvalue > maxHeatingSetpoint) { + targetvalue = maxHeatingSetpoint + } else if (mode == "cool" && targetvalue > maxCoolingSetpoint) { + targetvalue = maxCoolingSetpoint } sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false) @@ -543,25 +589,29 @@ void raiseSetpoint() { void lowerSetpoint() { def mode = device.currentValue("thermostatMode") def targetvalue + def minHeatingSetpoint = device.currentValue("minHeatingSetpoint") + def minCoolingSetpoint = device.currentValue("minCoolingSetpoint") + if (mode == "off" || mode == "auto") { log.warn "this mode: $mode does not allow lowerSetpoint" } else { - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() - def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() - def thermostatSetpoint = device.currentValue("thermostatSetpoint").toInteger() + def heatingSetpoint = device.currentValue("heatingSetpoint") + def coolingSetpoint = device.currentValue("coolingSetpoint") + def thermostatSetpoint = device.currentValue("thermostatSetpoint") log.debug "lowerSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}" if (device.latestState('thermostatSetpoint')) { - targetvalue = device.latestState('thermostatSetpoint').value as Integer + targetvalue = device.latestState('thermostatSetpoint').value + targetvalue = location.temperatureScale == "F"? targetvalue.toInteger() : targetvalue.toDouble() } else { targetvalue = 0 } - targetvalue = targetvalue - 1 + targetvalue = location.temperatureScale == "F"? targetvalue - 1 : targetvalue - 0.5 - if (mode == "heat" && targetvalue.toInteger() < 45) { - targetvalue = 45 - } else if (mode == "cool" && targetvalue.toInteger() < 65) { - targetvalue = 65 + if ((mode == "heat" || mode == "auxHeatOnly") && targetvalue < minHeatingSetpoint) { + targetvalue = minHeatingSetpoint + } else if (mode == "cool" && targetvalue < minCoolingSetpoint) { + targetvalue = minCoolingSetpoint } sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false) @@ -575,15 +625,15 @@ void lowerSetpoint() { void alterSetpoint(temp) { def mode = device.currentValue("thermostatMode") - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() - def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() + def heatingSetpoint = device.currentValue("heatingSetpoint") + def coolingSetpoint = device.currentValue("coolingSetpoint") def deviceId = device.deviceNetworkId.split(/\./).last() def targetHeatingSetpoint def targetCoolingSetpoint //step1: check thermostatMode, enforce limits before sending request to cloud - if (mode == "heat"){ + if (mode == "heat" || mode == "auxHeatOnly"){ if (temp.value > coolingSetpoint){ targetHeatingSetpoint = temp.value targetCoolingSetpoint = temp.value @@ -602,19 +652,22 @@ void alterSetpoint(temp) { } } - log.debug "alterSetpoint >> in mode ${mode} trying to change heatingSetpoint to ${targetHeatingSetpoint} " + - "coolingSetpoint to ${targetCoolingSetpoint} with holdType : ${holdType}" + log.debug "alterSetpoint >> in mode ${mode} trying to change heatingSetpoint to $targetHeatingSetpoint " + + "coolingSetpoint to $targetCoolingSetpoint with holdType : ${holdType}" def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite" - //step2: call parent.setHold to send http request to 3rd party cloud - if (parent.setHold(this, targetHeatingSetpoint, targetCoolingSetpoint, deviceId, sendHoldType)) { - sendEvent("name": "thermostatSetpoint", "value": temp.value.toString(), displayed: false) + + def coolingValue = location.temperatureScale == "C"? convertCtoF(targetCoolingSetpoint) : targetCoolingSetpoint + def heatingValue = location.temperatureScale == "C"? convertCtoF(targetHeatingSetpoint) : targetHeatingSetpoint + + if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) { + sendEvent("name": "thermostatSetpoint", "value": temp.value, displayed: false) sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint) sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint) log.debug "alterSetpoint in mode $mode succeed change setpoint to= ${temp.value}" } else { log.error "Error alterSetpoint()" - if (mode == "heat"){ + if (mode == "heat" || mode == "auxHeatOnly"){ sendEvent("name": "thermostatSetpoint", "value": heatingSetpoint.toString(), displayed: false) } else if (mode == "cool") { sendEvent("name": "thermostatSetpoint", "value": coolingSetpoint.toString(), displayed: false) @@ -626,9 +679,9 @@ void alterSetpoint(temp) { def generateStatusEvent() { def mode = device.currentValue("thermostatMode") - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() - def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() - def temperature = device.currentValue("temperature").toInteger() + def heatingSetpoint = device.currentValue("heatingSetpoint") + def coolingSetpoint = device.currentValue("coolingSetpoint") + def temperature = device.currentValue("temperature") def statusText @@ -643,14 +696,14 @@ def generateStatusEvent() { if (temperature >= heatingSetpoint) statusText = "Right Now: Idle" else - statusText = "Heating to ${heatingSetpoint}° F" + statusText = "Heating to ${heatingSetpoint} ${location.temperatureScale}" } else if (mode == "cool") { if (temperature <= coolingSetpoint) statusText = "Right Now: Idle" else - statusText = "Cooling to ${coolingSetpoint}° F" + statusText = "Cooling to ${coolingSetpoint} ${location.temperatureScale}" } else if (mode == "auto") { @@ -660,7 +713,7 @@ def generateStatusEvent() { statusText = "Right Now: Off" - } else if (mode == "emergencyHeat") { + } else if (mode == "auxHeatOnly") { statusText = "Emergency Heat" @@ -673,7 +726,18 @@ def generateStatusEvent() { sendEvent("name":"thermostatStatus", "value":statusText, "description":statusText, displayed: true) } -//generate custom mobile activity feeds event def generateActivityFeedsEvent(notificationMessage) { sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true) } + +def roundC (tempC) { + return (Math.round(tempC.toDouble() * 2))/2 +} + +def convertFtoC (tempF) { + return String.format("%.1f", (Math.round(((tempF - 32)*(5/9)) * 2))/2) +} + +def convertCtoF (tempC) { + return (Math.round(tempC * (9/5)) + 32).toInteger() +} diff --git a/devicetypes/smartthings/smartsense-moisture.src/smartsense-moisture.groovy b/devicetypes/smartthings/smartsense-moisture.src/smartsense-moisture.groovy index 5483cb1..43577f1 100644 --- a/devicetypes/smartthings/smartsense-moisture.src/smartsense-moisture.groovy +++ b/devicetypes/smartthings/smartsense-moisture.src/smartsense-moisture.groovy @@ -16,6 +16,7 @@ metadata { capability "Water Sensor" capability "Sensor" capability "Battery" + capability "Temperature Measurement" fingerprint deviceId: "0x2001", inClusters: "0x30,0x9C,0x9D,0x85,0x80,0x72,0x31,0x84,0x86" fingerprint deviceId: "0x2101", inClusters: "0x71,0x70,0x85,0x80,0x72,0x31,0x84,0x86" @@ -39,17 +40,29 @@ metadata { attributeState "wet", label: "Wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0" } } - standardTile("temperature", "device.temperature", width: 2, height: 2) { + standardTile("temperatureState", "device.temperature", width: 2, height: 2) { state "normal", icon:"st.alarm.temperature.normal", backgroundColor:"#ffffff" state "freezing", icon:"st.alarm.temperature.freeze", backgroundColor:"#53a7c0" state "overheated", icon:"st.alarm.temperature.overheat", backgroundColor:"#F80000" } + valueTile("temperature", "device.temperature", width: 2, height: 2) { + state("temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + ) + } valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { state "battery", label:'${currentValue}% battery', unit:"" } - - main (["water", "temperature"]) - details(["water", "temperature", "battery"]) + main (["water", "temperatureState"]) + details(["water", "temperatureState", "temperature", "battery"]) } } @@ -115,7 +128,7 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) map.descriptionText = "${device.displayName} is ${map.value}" } if(cmd.zwaveAlarmType == physicalgraph.zwave.commands.alarmv2.AlarmReport.ZWAVE_ALARM_TYPE_HEAT) { - map.name = "temperature" + map.name = "temperatureState" if(cmd.zwaveAlarmEvent == 1) { map.value = "overheated"} if(cmd.zwaveAlarmEvent == 2) { map.value = "overheated"} if(cmd.zwaveAlarmEvent == 3) { map.value = "changing temperature rapidly"} @@ -129,17 +142,30 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) map } -def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { def map = [:] - map.name = "water" - map.value = cmd.value ? "wet" : "dry" - map.descriptionText = "${device.displayName} is ${map.value}" + if(cmd.sensorType == 1) { + map.name = "temperature" + if(cmd.scale == 0) { + map.value = getTemperature(cmd.scaledSensorValue) + } else { + map.value = cmd.scaledSensorValue + } + map.unit = location.temperatureScale + } map } +def getTemperature(value) { + if(location.temperatureScale == "C"){ + return value + } else { + return Math.round(celsiusToFahrenheit(value)) + } +} + def zwaveEvent(physicalgraph.zwave.Command cmd) { log.debug "COMMAND CLASS: $cmd" -} - +} \ No newline at end of file diff --git a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy index 474a4f2..11d6f55 100644 --- a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy +++ b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy @@ -28,7 +28,7 @@ definition( category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png", - singleInstance: true + singleInstance: true ) { appSetting "clientId" } @@ -61,7 +61,7 @@ def authPage() { description = "Click to enter Ecobee Credentials" } - def redirectUrl = buildRedirectUrl //"${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}" + def redirectUrl = buildRedirectUrl log.debug "RedirectUrl = ${redirectUrl}" // get rid of next button until the user is actually auth'd if (!oauthTokenProvided) { @@ -103,7 +103,7 @@ def oauthInitUrl() { scope: "smartRead,smartWrite", client_id: smartThingsClientId, state: atomicState.oauthInitState, - redirect_uri: callbackUrl //"https://graph.api.smartthings.com/oauth/callback" + redirect_uri: callbackUrl ] redirect(location: "${apiEndpoint}/authorize?${toQueryString(oauthParams)}") @@ -115,14 +115,13 @@ def callback() { def code = params.code def oauthState = params.state - //verify oauthState == atomicState.oauthInitState, so the callback corresponds to the authentication request if (oauthState == atomicState.oauthInitState){ def tokenParams = [ grant_type: "authorization_code", code : code, client_id : smartThingsClientId, - redirect_uri: callbackUrl //"https://graph.api.smartthings.com/oauth/callback" + redirect_uri: callbackUrl ] def tokenUrl = "https://www.ecobee.com/home/token?${toQueryString(tokenParams)}" @@ -247,32 +246,32 @@ def getEcobeeThermostats() { ] def stats = [:] - try { - httpGet(deviceListParams) { resp -> + try { + httpGet(deviceListParams) { resp -> - if (resp.status == 200) { - resp.data.thermostatList.each { stat -> - atomicState.remoteSensors = stat.remoteSensors - def dni = [app.id, stat.identifier].join('.') - stats[dni] = getThermostatDisplayName(stat) - } - } else { - log.debug "http status: ${resp.status}" - //refresh the auth token - if (resp.status == 500 && resp.data.status.code == 14) { - log.debug "Storing the failed action to try later" - atomicState.action = "getEcobeeThermostats" - log.debug "Refreshing your auth_token!" - refreshAuthToken() - } else { - log.error "Authentication error, invalid authentication method, lack of credentials, etc." - } - } - } - } catch(Exception e) { - log.debug "___exception getEcobeeThermostats(): " + e - refreshAuthToken() - } + if (resp.status == 200) { + resp.data.thermostatList.each { stat -> + atomicState.remoteSensors = stat.remoteSensors + def dni = [app.id, stat.identifier].join('.') + stats[dni] = getThermostatDisplayName(stat) + } + } else { + log.debug "http status: ${resp.status}" + //refresh the auth token + if (resp.data.status.code == 14) { + log.debug "Storing the failed action to try later" + atomicState.action = "getEcobeeThermostats" + log.debug "Refreshing your auth_token!" + refreshAuthToken() + } else { + log.error "Authentication error, invalid authentication method, lack of credentials, etc." + } + } + } + } catch(Exception e) { + log.debug "___exception getEcobeeThermostats(): " + e + refreshAuthToken() + } atomicState.thermostats = stats return stats } @@ -317,7 +316,7 @@ def initialize() { def devices = thermostats.collect { dni -> def d = getChildDevice(dni) if(!d) { - d = addChildDevice(app.namespace, getChildName(), dni, null, ["label":"Ecobee Thermostat:${atomicState.thermostats[dni]}"]) + d = addChildDevice(app.namespace, getChildName(), dni, null, ["label":"${atomicState.thermostats[dni]}" ?: "Ecobee Thermostat"]) log.debug "created ${d.displayName} with id $dni" } else { log.debug "found ${d.displayName} with id $dni already exists" @@ -328,7 +327,7 @@ def initialize() { def sensors = ecobeesensors.collect { dni -> def d = getChildDevice(dni) if(!d) { - d = addChildDevice(app.namespace, getSensorChildName(), dni, null, ["label":"Ecobee Sensor:${atomicState.sensors[dni]}"]) + d = addChildDevice(app.namespace, getSensorChildName(), dni, null, ["label":"${atomicState.sensors[dni]}" ?:"Ecobee Sensor"]) log.debug "created ${d.displayName} with id $dni" } else { log.debug "found ${d.displayName} with id $dni already exists" @@ -354,21 +353,17 @@ def initialize() { atomicState.thermostatData = [:] //reset Map to store thermostat data - //send activity feeds to tell that device is connected - def notificationMessage = "is connected to SmartThings" - sendActivityFeeds(notificationMessage) - state.timeSendPush = null + //send activity feeds to tell that device is connected + def notificationMessage = "is connected to SmartThings" + sendActivityFeeds(notificationMessage) + atomicState.timeSendPush = null + atomicState.reAttempt = 0 pollHandler() //first time polling data data from thermostat //automatically update devices status every 5 mins runEvery5Minutes("poll") - //since access_token expires every 2 hours - runEvery1Hour("refreshAuthToken") - - atomicState.reAttempt = 0 - } def pollHandler() { @@ -389,18 +384,10 @@ def pollHandler() { def pollChildren(child = null) { def thermostatIdsString = getChildDeviceIdsString() log.debug "polling children: $thermostatIdsString" + def data = "" def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeExtendedRuntime":"true","includeSettings":"true","includeRuntime":"true","includeSensors":true}}' def result = false - // // TODO: test this: - // - // def jsonRequestBody = toJson([ - // selection:[ - // selectionType: "thermostats", - // selectionMatch: getChildDeviceIdsString(), - // includeRuntime: true - // ] - // ]) def pollParams = [ uri: apiEndpoint, @@ -411,11 +398,6 @@ def pollChildren(child = null) { try{ httpGet(pollParams) { resp -> - -// if (resp.data) { -// debugEventFromParent(child, "pollChildren(child) >> resp.status = ${resp.status}, resp.data = ${resp.data}") -// } - if(resp.status == 200) { log.debug "poll results returned resp.data ${resp.data}" atomicState.remoteSensors = resp.data.thermostatList.remoteSensors @@ -426,20 +408,41 @@ def pollChildren(child = null) { log.debug "updating dni $dni" - def data = [ + data = [ coolMode: (stat.settings.coolStages > 0), heatMode: (stat.settings.heatStages > 0), + deviceTemperatureUnit: stat.settings.useCelsius, + minHeatingSetpoint: (stat.settings.heatRangeLow / 10), + maxHeatingSetpoint: (stat.settings.heatRangeHigh / 10), + minCoolingSetpoint: (stat.settings.coolRangeLow / 10), + maxCoolingSetpoint: (stat.settings.coolRangeHigh / 10), autoMode: stat.settings.autoHeatCoolFeatureEnabled, auxHeatMode: (stat.settings.hasHeatPump) && (stat.settings.hasForcedAir || stat.settings.hasElectric || stat.settings.hasBoiler), - temperature: stat.runtime.actualTemperature / 10, + temperature: (stat.runtime.actualTemperature / 10), heatingSetpoint: stat.runtime.desiredHeat / 10, coolingSetpoint: stat.runtime.desiredCool / 10, - thermostatMode: stat.settings.hvacMode + thermostatMode: stat.settings.hvacMode, + humidity: stat.runtime.actualHumidity ] - data["temperature"] = data["temperature"] ? data["temperature"].toDouble().toInteger() : data["temperature"] - data["heatingSetpoint"] = data["heatingSetpoint"] ? data["heatingSetpoint"].toDouble().toInteger() : data["heatingSetpoint"] - data["coolingSetpoint"] = data["coolingSetpoint"] ? data["coolingSetpoint"].toDouble().toInteger() : data["coolingSetpoint"] -// debugEventFromParent(child, "Event Data = ${data}") + + if (location.temperatureScale == "F") + { + data["temperature"] = data["temperature"] ? Math.round(data["temperature"].toDouble()) : data["temperature"] + data["heatingSetpoint"] = data["heatingSetpoint"] ? Math.round(data["heatingSetpoint"].toDouble()) : data["heatingSetpoint"] + data["coolingSetpoint"] = data["coolingSetpoint"] ? Math.round(data["coolingSetpoint"].toDouble()) : data["coolingSetpoint"] + data["minHeatingSetpoint"] = data["minHeatingSetpoint"] ? Math.round(data["minHeatingSetpoint"].toDouble()) : data["minHeatingSetpoint"] + data["maxHeatingSetpoint"] = data["maxHeatingSetpoint"] ? Math.round(data["maxHeatingSetpoint"].toDouble()) : data["maxHeatingSetpoint"] + data["minCoolingSetpoint"] = data["minCoolingSetpoint"] ? Math.round(data["minCoolingSetpoint"].toDouble()) : data["minCoolingSetpoint"] + data["maxCoolingSetpoint"] = data["maxCoolingSetpoint"] ? Math.round(data["maxCoolingSetpoint"].toDouble()) : data["maxCoolingSetpoint"] + + } + + if (data?.deviceTemperatureUnit == false && location.temperatureScale == "F") { + data["deviceTemperatureUnit"] = "F" + + } else { + data["deviceTemperatureUnit"] = "C" + } collector[dni] = [data:data] return collector @@ -450,9 +453,8 @@ def pollChildren(child = null) { log.error "polling children & got http status ${resp.status}" //refresh the auth token - if (resp.status == 500 && resp.data.status.code == 14) { - log.debug "Storing the failed action to try later" - atomicState.action = "pollChildren"; + if (resp.data.status.code == 14) { + atomicState.action = "pollChildren" log.debug "Refreshing your auth_token!" refreshAuthToken() } @@ -463,7 +465,6 @@ def pollChildren(child = null) { } } catch(Exception e) { log.debug "___exception polling children: " + e -// debugEventFromParent(child, "___exception polling children: " + e) refreshAuthToken() } return result @@ -476,18 +477,14 @@ def pollChild(child){ if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")){ if(atomicState.thermostats[child.device.deviceNetworkId] != null) { def tData = atomicState.thermostats[child.device.deviceNetworkId] -// debugEventFromParent(child, "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}") //TODO comment log.info "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}" child.generateEvent(tData.data) //parse received message from parent -// return tData.data } else if(atomicState.thermostats[child.device.deviceNetworkId] == null) { -// debugEventFromParent(child, "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling") //TODO comment log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId}" return null } } } else { -// debugEventFromParent(child, "ERROR: pollChildren(child) for ${child.device.deviceNetworkId} after polling") //TODO comment log.info "ERROR: pollChildren(child) for ${child.device.deviceNetworkId} after polling" return null } @@ -513,9 +510,6 @@ def availableModes(child) { { log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" - // TODO: flag device as in error state - // child.errorState = true - return null } @@ -542,8 +536,6 @@ def currentMode(child) { if(!tData) { log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" - // TODO: flag device as in error state - // child.errorState = true return null } @@ -561,8 +553,12 @@ def updateSensorData() { def occupancy = "" it.capability.each { if (it.type == "temperature") { - temperature = it.value as Double - temperature = (temperature / 10).toInteger() + if (location.temperatureScale == "F") { + temperature = Math.round(it.value.toDouble() / 10) + } else { + temperature = convertFtoC(it.value.toDouble() / 10) + } + } else if (it.type == "occupancy") { if(it.value == "true") occupancy = "active" @@ -575,7 +571,6 @@ def updateSensorData() { if(d) { d.sendEvent(name:"temperature", value: temperature) d.sendEvent(name:"motion", value: occupancy) -// debugEventFromParent(d, "temperature : ${temperature}, motion:${occupancy}") } } } @@ -595,64 +590,63 @@ def toQueryString(Map m) { } private refreshAuthToken() { - log.debug "refreshing auth token" + log.debug "refreshing auth token" - if(!atomicState.refreshToken) { - log.warn "Can not refresh OAuth token since there is no refreshToken stored" - } else { + if(!atomicState.refreshToken) { + log.warn "Can not refresh OAuth token since there is no refreshToken stored" + } else { - def refreshParams = [ - method: 'POST', - uri : apiEndpoint, - path : "/token", - query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId], - ] + def refreshParams = [ + method: 'POST', + uri : apiEndpoint, + path : "/token", + query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId], + ] - log.debug refreshParams + log.debug refreshParams - def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials." - //changed to httpPost - try { - def jsonMap - httpPost(refreshParams) { resp -> + def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials." + //changed to httpPost + try { + def jsonMap + httpPost(refreshParams) { resp -> - if(resp.status == 200) { - log.debug "Token refreshed...calling saved RestAction now!" + if(resp.status == 200) { + log.debug "Token refreshed...calling saved RestAction now!" - debugEvent("Token refreshed ... calling saved RestAction now!") + debugEvent("Token refreshed ... calling saved RestAction now!") - log.debug resp + log.debug resp - jsonMap = resp.data + jsonMap = resp.data - if(resp.data) { + if(resp.data) { - log.debug resp.data - debugEvent("Response = ${resp.data}") + log.debug resp.data + debugEvent("Response = ${resp.data}") - atomicState.refreshToken = resp?.data?.refresh_token - atomicState.authToken = resp?.data?.access_token + atomicState.refreshToken = resp?.data?.refresh_token + atomicState.authToken = resp?.data?.access_token - debugEvent("Refresh Token = ${atomicState.refreshToken}") - debugEvent("OAUTH Token = ${atomicState.authToken}") + debugEvent("Refresh Token = ${atomicState.refreshToken}") + debugEvent("OAUTH Token = ${atomicState.authToken}") - if(atomicState.action && atomicState.action != "") { - log.debug "Executing next action: ${atomicState.action}" + if(atomicState.action && atomicState.action != "") { + log.debug "Executing next action: ${atomicState.action}" - "${atomicState.action}"() + "${atomicState.action}"() - //remove saved action - atomicState.action = "" - } + atomicState.action = "" + } - } - atomicState.action = "" - } else { - log.debug "refresh failed ${resp.status} : ${resp.status.code}" - } - } - } catch(Exception e) { - log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}" + } + atomicState.action = "" + } else { + log.debug "refresh failed ${resp.status} : ${resp.status.code}" + } + } + } catch (groovyx.net.http.HttpResponseException e) { + log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}" def reAttemptPeriod = 300 // in sec if (e.statusCode != 401) { //this issue might comes from exceed 20sec app execution, connectivity issue etc. runIn(reAttemptPeriod, "refreshAuthToken") @@ -665,20 +659,16 @@ private refreshAuthToken() { sendPushAndFeeds(notificationMessage) atomicState.reAttempt = 0 } - } - } - } + } + } + } } def resumeProgram(child, deviceId) { -// def thermostatIdsString = getChildDeviceIdsString() -// log.debug "resumeProgram children: $thermostatIdsString" def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}]}' - //, { "type": "sendMessage", "params": { "text": "Setpoint Updated" } } def result = sendJson(jsonRequestBody) -// debugEventFromParent(child, "resumeProgram(child) with result ${result}") return result } @@ -687,27 +677,16 @@ def setHold(child, heating, cooling, deviceId, sendHoldType) { int h = heating * 10 int c = cooling * 10 -// log.debug "setpoints____________ - h: $heating - $h, c: $cooling - $c" -// def thermostatIdsString = getChildDeviceIdsString() def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": '+sendHoldType+' } } ]}' -// def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}, { "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": "indefinite" } } ]}' def result = sendJson(child, jsonRequestBody) -// debugEventFromParent(child, "setHold: heating: ${h}, cooling: ${c} with result ${result}") return result } def setMode(child, mode, deviceId) { -// def thermostatIdsString = getChildDeviceIdsString() -// log.debug "setCoolingSetpoint children: $thermostatIdsString" - def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"thermostat": {"settings":{"hvacMode":"'+"${mode}"+'"}}}' -// log.debug "Mode Request Body = ${jsonRequestBody}" -// debugEvent ("Mode Request Body = ${jsonRequestBody}") - def result = sendJson(jsonRequestBody) -// debugEventFromParent(child, "setMode to ${mode} with result ${result}") return result } @@ -724,8 +703,6 @@ def sendJson(child = null, String jsonBody) { try{ httpPost(cmdParams) { resp -> -// debugEventFromParent(child, "sendJson >> resp.status ${resp.status}, resp.data: ${resp.data}") - if(resp.status == 200) { log.debug "updated ${resp.data}" @@ -741,8 +718,7 @@ def sendJson(child = null, String jsonBody) { debugEvent ("sent Json & got http status ${resp.status} - ${resp.status.code}") //refresh the auth token - if (resp.status == 500 && resp.status.code == 14) { - //log.debug "Storing the failed action to try later" + if (resp.status.code == 14) { log.debug "Refreshing your auth_token!" debugEvent ("Refreshing OAUTH Token") refreshAuthToken() @@ -757,7 +733,7 @@ def sendJson(child = null, String jsonBody) { } catch(Exception e) { log.debug "Exception Sending Json: " + e debugEvent ("Exception Sending JSON: " + e) - refreshAuthToken() + refreshAuthToken() return false } @@ -794,25 +770,37 @@ def debugEventFromParent(child, message) { //send both push notification and mobile activity feeds def sendPushAndFeeds(notificationMessage){ - log.warn "sendPushAndFeeds >> notificationMessage: ${notificationMessage}" - log.warn "sendPushAndFeeds >> atomicState.timeSendPush: ${atomicState.timeSendPush}" - if (atomicState.timeSendPush){ - if (now() - atomicState.timeSendPush > 86400000){ // notification is sent to remind user once a day - sendPush("Your Ecobee thermostat " + notificationMessage) - sendActivityFeeds(notificationMessage) - atomicState.timeSendPush = now() - } - } else { - sendPush("Your Ecobee thermostat " + notificationMessage) - sendActivityFeeds(notificationMessage) - atomicState.timeSendPush = now() - } - atomicState.authToken = null + log.warn "sendPushAndFeeds >> notificationMessage: ${notificationMessage}" + log.warn "sendPushAndFeeds >> atomicState.timeSendPush: ${atomicState.timeSendPush}" + if (atomicState.timeSendPush){ + if (now() - atomicState.timeSendPush > 86400000){ // notification is sent to remind user once a day + sendPush("Your Ecobee thermostat " + notificationMessage) + sendActivityFeeds(notificationMessage) + atomicState.timeSendPush = now() + } + } else { + sendPush("Your Ecobee thermostat " + notificationMessage) + sendActivityFeeds(notificationMessage) + atomicState.timeSendPush = now() + } + atomicState.authToken = null } def sendActivityFeeds(notificationMessage) { - def devices = getChildDevices() - devices.each { child -> - child.generateActivityFeedsEvent(notificationMessage) //parse received message from parent - } + def devices = getChildDevices() + devices.each { child -> + child.generateActivityFeedsEvent(notificationMessage) //parse received message from parent + } +} + +def roundC (tempC) { + return String.format("%.1f", (Math.round(tempC * 2))/2) +} + +def convertFtoC (tempF) { + return String.format("%.1f", (Math.round(((tempF - 32)*(5/9)) * 2))/2) +} + +def convertCtoF (tempC) { + return (Math.round(tempC * (9/5)) + 32).toInteger() } diff --git a/smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy b/smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy index 9b5f11c..11f295c 100644 --- a/smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy +++ b/smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy @@ -201,8 +201,8 @@ def completionPage() { section("Switches") { input(name: "completionSwitches", type: "capability.switch", title: "Set these switches", description: null, required: false, multiple: true, submitOnChange: true) - if (completionSwitches || androidClient()) { - input(name: "completionSwitchesState", type: "enum", title: "To", description: null, required: false, multiple: false, options: ["on", "off"], style: "segmented", defaultValue: "on") + if (completionSwitches) { + input(name: "completionSwitchesState", type: "enum", title: "To", description: null, required: false, multiple: false, options: ["on", "off"], defaultValue: "on") input(name: "completionSwitchesLevel", type: "number", title: "Optionally, Set Dimmer Levels To", description: null, required: false, multiple: false, range: "(0..99)") } } diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index 053c266..b8d3f54 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -35,23 +35,11 @@ preferences { } def mainPage() { - if(canInstallLabs()) { - def bridges = bridgesDiscovered() - if (state.username && bridges) { - return bulbDiscovery() - } else { - return bridgeDiscovery() - } + def bridges = bridgesDiscovered() + if (state.username && bridges) { + return bulbDiscovery() } else { - def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. - -To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub".""" - - return dynamicPage(name:"bridgeDiscovery", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) { - section("Upgrade") { - paragraph "$upgradeNeeded" - } - } + return bridgeDiscovery() } } @@ -326,6 +314,7 @@ def addBulbs() { d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name]) } log.debug "created ${d.displayName} with id $dni" + d.refresh() } else { log.debug "$dni in not longer paired to the Hue Bridge or ID changed" } @@ -333,8 +322,8 @@ def addBulbs() { //backwards compatable newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni } d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name]) + d.refresh() } - d.refresh() } else { log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'" if (bulbs instanceof java.util.Map) { @@ -764,14 +753,10 @@ private String convertHexToIP(hex) { [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") } -private Boolean canInstallLabs() { - return hasAllHubsOver("000.011.00603") -} - private Boolean hasAllHubsOver(String desiredFirmware) { return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } } private List getRealHubFirmwareVersions() { return location.hubs*.firmwareVersionString.findAll { it } -} \ No newline at end of file +} diff --git a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy index 03c5ccc..9fc1a5d 100644 --- a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy +++ b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy @@ -419,9 +419,11 @@ def addDevice() { def d = getChildDevice(dni) if(!d) { def newAction = state.HarmonyActivities.find { it.key == dni } - d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"]) - log.trace "created ${d.displayName} with id $dni" - poll() + if (newAction) { + d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"]) + log.trace "created ${d.displayName} with id $dni" + poll() + } } else { log.trace "found ${d.displayName} with id $dni already exists" } diff --git a/smartapps/smartthings/notify-me-when.src/i18n/messages.properties b/smartapps/smartthings/notify-me-when.src/i18n/messages.properties new file mode 100644 index 0000000..a190968 --- /dev/null +++ b/smartapps/smartthings/notify-me-when.src/i18n/messages.properties @@ -0,0 +1,31 @@ +'''Acceleration Detected'''.ko=가속화 감지됨 +'''Arrival Of'''.ko=도착 +'''Both Push and SMS?'''.ko=푸시 메시지와 SMS를 모두 사용하시겠습니까? +'''Button Pushed'''.ko=버튼이 눌렸습니다 +'''Contact Closes'''.ko=접점 닫힘 +'''Contact Opens'''.ko=접점 열림 +'''Departure Of'''.ko=출발 +'''Message Text'''.ko=문자 메시지 +'''Minutes'''.ko=분 +'''Motion Here'''.ko=동작 +'''Phone Number (for SMS, optional)'''.ko=휴대전화 번호(문자 메시지 - 옵션) +'''Receive notifications when anything happens in your home.'''.ko=집 안에 무슨 일이 일어나면 알림이 전송됩니다. +'''Smoke Detected'''.ko=연기가 감지되었습니다 +'''Switch Turned Off'''.ko=스위치 꺼짐 +'''Switch Turned On'''.ko=스위치 꺼짐 +'''Choose one or more, when...'''.ko=다음의 경우 하나 이상 선택 +'''Yes'''.ko=예 +'''No'''.ko=아니요 +'''Send this message (optional, sends standard status message if not specified)'''.ko=이 메시지 전송(선택적, 지정되지 않은 경우 표준 상태 메시지를 보냅니다) +'''Via a push notification and/or an SMS message'''.ko=푸시 알림 및/또는 문자 메시지를 통해 +'''Set for specific mode(s)'''.ko=특정 모드 설정 +'''Tap to set'''.ko=눌러서 설정 +'''Minimum time between messages (optional, defaults to every message)'''.ko=메시지작 간 최소 시간(선택 사항, 모든 메시지의 기본 설정) +'''If outside the US please make sure to enter the proper country code'''.ko=미국 이외 거주자는 적절한 국가 코드를 입력했는지 확인하십시오 +'''Water Sensor Wet'''.ko=Water Sensor에서 물이 감지되었습니다 +'''{{ triggerEvent.linkText }} has arrived at the {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}에 도착했습니다 +'''{{ triggerEvent.linkText }} has arrived at {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}에 도착했습니다 +'''{{ triggerEvent.linkText }} has left the {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}을(를) 떠났습니다 +'''{{ triggerEvent.linkText }} has left {{ location.name }}'''.ko={{ triggerEvent.linkText }}님이 {{ location.name }}을(를) 떠났습니다 +'''Assign a name'''.ko=이름 배정 +'''Choose Modes'''.ko=모드 선택 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 12fa2a7..f124260 100644 --- a/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy +++ b/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy @@ -20,19 +20,19 @@ * 2014-10-03: Added capability.button device picker and button.pushed event subscription. For Doorbell. */ definition( - name: "Notify Me When", - namespace: "smartthings", - author: "SmartThings", - description: "Receive notifications when anything happens in your home.", - category: "Convenience", - iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact@2x.png" + name: "Notify Me When", + namespace: "smartthings", + author: "SmartThings", + description: "Receive notifications when anything happens in your home.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact@2x.png" ) preferences { section("Choose one or more, when..."){ input "button", "capability.button", title: "Button Pushed", required: false, multiple: true //tw - input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true input "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true input "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true input "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true @@ -47,11 +47,11 @@ preferences { input "messageText", "text", title: "Message Text", required: false } section("Via a push notification and/or an SMS message"){ - input("recipients", "contact", title: "Send notifications to") { - input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false - paragraph "If outside the US please make sure to enter the proper country code" - input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes", "No"] - } + input("recipients", "contact", title: "Send notifications to") { + input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false + paragraph "If outside the US please make sure to enter the proper country code" + input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes", "No"] + } } section("Minimum time between messages (optional, defaults to every message)") { input "frequency", "decimal", title: "Minutes", required: false @@ -71,7 +71,7 @@ def updated() { def subscribeToEvents() { subscribe(button, "button.pushed", eventHandler) //tw - subscribe(contact, "contact.open", eventHandler) + subscribe(contact, "contact.open", eventHandler) subscribe(contactClosed, "contact.closed", eventHandler) subscribe(acceleration, "acceleration.active", eventHandler) subscribe(motion, "motion.active", eventHandler) @@ -99,49 +99,55 @@ def eventHandler(evt) { } private sendMessage(evt) { - def msg = messageText ?: defaultText(evt) + String msg = messageText + Map options = [:] + + if (!messageText) { + msg = defaultText(evt) + options = [translatable: true, triggerEvent: evt] + } log.debug "$evt.name:$evt.value, pushAndPhone:$pushAndPhone, '$msg'" - if (location.contactBookEnabled) { - sendNotificationToContacts(msg, recipients) - } - else { + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients, options) + } else { + if (!phone || pushAndPhone != 'No') { + log.debug 'sending push' + options.method = 'push' + //sendPush(msg) + } + if (phone) { + options.phone = phone + log.debug 'sending SMS' + //sendSms(phone, msg) + } + sendNotification(msg, options) + } - if (!phone || pushAndPhone != "No") { - log.debug "sending push" - sendPush(msg) - } - if (phone) { - log.debug "sending SMS" - sendSms(phone, msg) - } - } if (frequency) { state[evt.deviceId] = now() } } private defaultText(evt) { - if (evt.name == "presence") { - if (evt.value == "present") { + if (evt.name == 'presence') { + if (evt.value == 'present') { if (includeArticle) { - "$evt.linkText has arrived at the $location.name" + '{{ triggerEvent.linkText }} has arrived at the {{ location.name }}' } else { - "$evt.linkText has arrived at $location.name" + '{{ triggerEvent.linkText }} has arrived at {{ location.name }}' } - } - else { + } else { if (includeArticle) { - "$evt.linkText has left the $location.name" + '{{ triggerEvent.linkText }} has left the {{ location.name }}' } else { - "$evt.linkText has left $location.name" + '{{ triggerEvent.linkText }} has left {{ location.name }}' } } - } - else { - evt.descriptionText + } else { + '{{ triggerEvent.descriptionText }}' } }