mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-19 13:20:53 +00:00
Compare commits
1 Commits
MSA-1458-1
...
PROD_2016.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a22f71bc29 |
@@ -274,7 +274,6 @@ private Map makeTemperatureResult(value) {
|
|||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: "" + value,
|
value: "" + value,
|
||||||
descriptionText: "${linkText} is ${value}°${temperatureScale}",
|
descriptionText: "${linkText} is ${value}°${temperatureScale}",
|
||||||
unit: temperatureScale
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -254,8 +254,7 @@ private Map getTemperatureResult(value) {
|
|||||||
return [
|
return [
|
||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText,
|
descriptionText: descriptionText
|
||||||
unit: temperatureScale
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,9 +47,6 @@ metadata {
|
|||||||
|
|
||||||
command "everywhereJoin"
|
command "everywhereJoin"
|
||||||
command "everywhereLeave"
|
command "everywhereLeave"
|
||||||
|
|
||||||
command "forceOff"
|
|
||||||
command "forceOn"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,9 +64,9 @@ metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) {
|
standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) {
|
||||||
state "on", label: '${name}', action: "forceOff", icon: "st.Electronics.electronics16", backgroundColor: "#79b821", nextState:"turningOff"
|
state "on", label: '${name}', action: "switch.off", icon: "st.Electronics.electronics16", backgroundColor: "#79b821", nextState:"turningOff"
|
||||||
state "turningOff", label:'TURNING OFF', icon:"st.Electronics.electronics16", backgroundColor:"#ffffff"
|
state "turningOff", label:'TURNING OFF', icon:"st.Electronics.electronics16", backgroundColor:"#ffffff"
|
||||||
state "off", label: '${name}', action: "forceOn", icon: "st.Electronics.electronics16", backgroundColor: "#ffffff", nextState:"turningOn"
|
state "off", label: '${name}', action: "switch.on", icon: "st.Electronics.electronics16", backgroundColor: "#ffffff", nextState:"turningOn"
|
||||||
state "turningOn", label:'TURNING ON', icon:"st.Electronics.electronics16", backgroundColor:"#79b821"
|
state "turningOn", label:'TURNING ON', icon:"st.Electronics.electronics16", backgroundColor:"#79b821"
|
||||||
}
|
}
|
||||||
valueTile("1", "device.station1", decoration: "flat", canChangeIcon: false) {
|
valueTile("1", "device.station1", decoration: "flat", canChangeIcon: false) {
|
||||||
@@ -143,22 +140,8 @@ metadata {
|
|||||||
* one place.
|
* one place.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
def off() {
|
def off() { onAction("off") }
|
||||||
if (device.currentState("switch")?.value == "on") {
|
def on() { onAction("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 volup() { onAction("volup") }
|
||||||
def voldown() { onAction("voldown") }
|
def voldown() { onAction("voldown") }
|
||||||
def preset1() { onAction("1") }
|
def preset1() { onAction("1") }
|
||||||
@@ -257,11 +240,11 @@ def onAction(String user, data=null) {
|
|||||||
def actions = null
|
def actions = null
|
||||||
switch (user) {
|
switch (user) {
|
||||||
case "on":
|
case "on":
|
||||||
boseSetPowerState(true)
|
actions = boseSetPowerState(true)
|
||||||
break
|
break
|
||||||
case "off":
|
case "off":
|
||||||
boseSetNowPlaying(null, "STANDBY")
|
boseSetNowPlaying(null, "STANDBY")
|
||||||
boseSetPowerState(false)
|
actions = boseSetPowerState(false)
|
||||||
break
|
break
|
||||||
case "volume":
|
case "volume":
|
||||||
actions = boseSetVolume(data)
|
actions = boseSetVolume(data)
|
||||||
|
|||||||
@@ -89,17 +89,14 @@ def parse(String description) {
|
|||||||
log.debug "TEMP"
|
log.debug "TEMP"
|
||||||
map.name = "temperature"
|
map.name = "temperature"
|
||||||
map.value = getTemperature(descMap.value)
|
map.value = getTemperature(descMap.value)
|
||||||
map.unit = temperatureScale
|
|
||||||
} else if (descMap.cluster == "0201" && descMap.attrId == "0011") {
|
} else if (descMap.cluster == "0201" && descMap.attrId == "0011") {
|
||||||
log.debug "COOLING SETPOINT"
|
log.debug "COOLING SETPOINT"
|
||||||
map.name = "coolingSetpoint"
|
map.name = "coolingSetpoint"
|
||||||
map.value = getTemperature(descMap.value)
|
map.value = getTemperature(descMap.value)
|
||||||
map.unit = temperatureScale
|
|
||||||
} else if (descMap.cluster == "0201" && descMap.attrId == "0012") {
|
} else if (descMap.cluster == "0201" && descMap.attrId == "0012") {
|
||||||
log.debug "HEATING SETPOINT"
|
log.debug "HEATING SETPOINT"
|
||||||
map.name = "heatingSetpoint"
|
map.name = "heatingSetpoint"
|
||||||
map.value = getTemperature(descMap.value)
|
map.value = getTemperature(descMap.value)
|
||||||
map.unit = temperatureScale
|
|
||||||
} else if (descMap.cluster == "0201" && descMap.attrId == "001c") {
|
} else if (descMap.cluster == "0201" && descMap.attrId == "001c") {
|
||||||
log.debug "MODE"
|
log.debug "MODE"
|
||||||
map.name = "thermostatMode"
|
map.name = "thermostatMode"
|
||||||
@@ -172,7 +169,7 @@ def setHeatingSetpoint(degrees) {
|
|||||||
|
|
||||||
def degreesInteger = Math.round(degrees)
|
def degreesInteger = Math.round(degrees)
|
||||||
log.debug "setHeatingSetpoint({$degreesInteger} ${temperatureScale})"
|
log.debug "setHeatingSetpoint({$degreesInteger} ${temperatureScale})"
|
||||||
sendEvent("name": "heatingSetpoint", "value": degreesInteger, "unit": temperatureScale)
|
sendEvent("name": "heatingSetpoint", "value": degreesInteger)
|
||||||
|
|
||||||
def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
|
def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
|
||||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius * 100) + "}"
|
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius * 100) + "}"
|
||||||
@@ -183,7 +180,7 @@ def setCoolingSetpoint(degrees) {
|
|||||||
if (degrees != null) {
|
if (degrees != null) {
|
||||||
def degreesInteger = Math.round(degrees)
|
def degreesInteger = Math.round(degrees)
|
||||||
log.debug "setCoolingSetpoint({$degreesInteger} ${temperatureScale})"
|
log.debug "setCoolingSetpoint({$degreesInteger} ${temperatureScale})"
|
||||||
sendEvent("name": "coolingSetpoint", "value": degreesInteger, "unit": temperatureScale)
|
sendEvent("name": "coolingSetpoint", "value": degreesInteger)
|
||||||
def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
|
def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
|
||||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius * 100) + "}"
|
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius * 100) + "}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
.st-ignore
|
|
||||||
README.md
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# Connected Cree LED Bulb
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Works with:
|
|
||||||
|
|
||||||
* [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
|
|
||||||
|
|
||||||
## 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)
|
|
||||||
@@ -152,11 +152,11 @@ def generateEvent(Map results) {
|
|||||||
sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue
|
sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue
|
||||||
isChange = isTemperatureStateChange(device, name, value.toString())
|
isChange = isTemperatureStateChange(device, name, value.toString())
|
||||||
isDisplayed = isChange
|
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") {
|
} else if (name=="maxCoolingSetpoint" || name=="minCoolingSetpoint" || name=="maxHeatingSetpoint" || name=="minHeatingSetpoint") {
|
||||||
def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F
|
def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F
|
||||||
sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue
|
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"){
|
} else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){
|
||||||
isChange = isStateChange(device, name, value.toString())
|
isChange = isStateChange(device, name, value.toString())
|
||||||
event << [value: value.toString(), isStateChange: isChange, displayed: false]
|
event << [value: value.toString(), isStateChange: isChange, displayed: false]
|
||||||
@@ -234,9 +234,9 @@ void setHeatingSetpoint(setpoint) {
|
|||||||
def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint
|
def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint
|
||||||
|
|
||||||
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
|
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
|
||||||
if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) {
|
if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) {
|
||||||
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale)
|
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
|
||||||
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint, "unit":location.temperatureScale)
|
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
|
||||||
log.debug "Done setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}"
|
log.debug "Done setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}"
|
||||||
generateSetpointEvent()
|
generateSetpointEvent()
|
||||||
generateStatusEvent()
|
generateStatusEvent()
|
||||||
@@ -271,9 +271,9 @@ void setCoolingSetpoint(setpoint) {
|
|||||||
def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint
|
def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint
|
||||||
|
|
||||||
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
|
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
|
||||||
if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) {
|
if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) {
|
||||||
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale)
|
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
|
||||||
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint, "unit":location.temperatureScale)
|
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
|
||||||
log.debug "Done setCoolingSetpoint>> coolingSetpoint = ${coolingSetpoint}, heatingSetpoint = ${heatingSetpoint}"
|
log.debug "Done setCoolingSetpoint>> coolingSetpoint = ${coolingSetpoint}, heatingSetpoint = ${heatingSetpoint}"
|
||||||
generateSetpointEvent()
|
generateSetpointEvent()
|
||||||
generateStatusEvent()
|
generateStatusEvent()
|
||||||
@@ -287,14 +287,14 @@ void resumeProgram() {
|
|||||||
log.debug "resumeProgram() is called"
|
log.debug "resumeProgram() is called"
|
||||||
sendEvent("name":"thermostatStatus", "value":"resuming schedule", "description":statusText, displayed: false)
|
sendEvent("name":"thermostatStatus", "value":"resuming schedule", "description":statusText, displayed: false)
|
||||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
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)
|
sendEvent("name":"thermostatStatus", "value":"setpoint is updating", "description":statusText, displayed: false)
|
||||||
runIn(5, "poll")
|
runIn(5, "poll")
|
||||||
log.debug "resumeProgram() is done"
|
log.debug "resumeProgram() is done"
|
||||||
sendEvent("name":"resumeProgram", "value":"resume", descriptionText: "resumeProgram is done", displayed: false, isStateChange: true)
|
sendEvent("name":"resumeProgram", "value":"resume", descriptionText: "resumeProgram is done", displayed: false, isStateChange: true)
|
||||||
} else {
|
} else {
|
||||||
sendEvent("name":"thermostatStatus", "value":"failed resume click refresh", "description":statusText, displayed: false)
|
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() {
|
def off() {
|
||||||
log.debug "off"
|
log.debug "off"
|
||||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||||
if (parent.setMode ("off", deviceId))
|
if (parent.setMode (this,"off", deviceId))
|
||||||
generateModeEvent("off")
|
generateModeEvent("off")
|
||||||
else {
|
else {
|
||||||
log.debug "Error setting new mode."
|
log.debug "Error setting new mode."
|
||||||
@@ -420,7 +420,7 @@ def off() {
|
|||||||
def heat() {
|
def heat() {
|
||||||
log.debug "heat"
|
log.debug "heat"
|
||||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||||
if (parent.setMode ("heat", deviceId))
|
if (parent.setMode (this,"heat", deviceId))
|
||||||
generateModeEvent("heat")
|
generateModeEvent("heat")
|
||||||
else {
|
else {
|
||||||
log.debug "Error setting new mode."
|
log.debug "Error setting new mode."
|
||||||
@@ -438,7 +438,7 @@ def emergencyHeat() {
|
|||||||
def auxHeatOnly() {
|
def auxHeatOnly() {
|
||||||
log.debug "auxHeatOnly"
|
log.debug "auxHeatOnly"
|
||||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||||
if (parent.setMode ("auxHeatOnly", deviceId))
|
if (parent.setMode (this,"auxHeatOnly", deviceId))
|
||||||
generateModeEvent("auxHeatOnly")
|
generateModeEvent("auxHeatOnly")
|
||||||
else {
|
else {
|
||||||
log.debug "Error setting new mode."
|
log.debug "Error setting new mode."
|
||||||
@@ -452,7 +452,7 @@ def auxHeatOnly() {
|
|||||||
def cool() {
|
def cool() {
|
||||||
log.debug "cool"
|
log.debug "cool"
|
||||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||||
if (parent.setMode ("cool", deviceId))
|
if (parent.setMode (this,"cool", deviceId))
|
||||||
generateModeEvent("cool")
|
generateModeEvent("cool")
|
||||||
else {
|
else {
|
||||||
log.debug "Error setting new mode."
|
log.debug "Error setting new mode."
|
||||||
@@ -466,7 +466,7 @@ def cool() {
|
|||||||
def auto() {
|
def auto() {
|
||||||
log.debug "auto"
|
log.debug "auto"
|
||||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||||
if (parent.setMode ("auto", deviceId))
|
if (parent.setMode (this,"auto", deviceId))
|
||||||
generateModeEvent("auto")
|
generateModeEvent("auto")
|
||||||
else {
|
else {
|
||||||
log.debug "Error setting new mode."
|
log.debug "Error setting new mode."
|
||||||
@@ -489,7 +489,7 @@ def fanOn() {
|
|||||||
def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint
|
def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint
|
||||||
def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint
|
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)
|
generateFanModeEvent(fanMode)
|
||||||
} else {
|
} else {
|
||||||
log.debug "Error setting new mode."
|
log.debug "Error setting new mode."
|
||||||
@@ -510,7 +510,7 @@ def fanAuto() {
|
|||||||
def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint
|
def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint
|
||||||
def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint
|
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)
|
generateFanModeEvent(fanMode)
|
||||||
} else {
|
} else {
|
||||||
log.debug "Error setting new mode."
|
log.debug "Error setting new mode."
|
||||||
@@ -556,12 +556,12 @@ def generateSetpointEvent() {
|
|||||||
|
|
||||||
if (mode == "heat") {
|
if (mode == "heat") {
|
||||||
|
|
||||||
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale)
|
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint )
|
||||||
|
|
||||||
}
|
}
|
||||||
else if (mode == "cool") {
|
else if (mode == "cool") {
|
||||||
|
|
||||||
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint, "unit":location.temperatureScale)
|
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint)
|
||||||
|
|
||||||
} else if (mode == "auto") {
|
} else if (mode == "auto") {
|
||||||
|
|
||||||
@@ -573,7 +573,7 @@ def generateSetpointEvent() {
|
|||||||
|
|
||||||
} else if (mode == "auxHeatOnly") {
|
} 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
|
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"
|
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
|
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
|
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"
|
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
|
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 coolingValue = location.temperatureScale == "C"? convertCtoF(targetCoolingSetpoint) : targetCoolingSetpoint
|
||||||
def heatingValue = location.temperatureScale == "C"? convertCtoF(targetHeatingSetpoint) : targetHeatingSetpoint
|
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": "thermostatSetpoint", "value": temp.value, displayed: false)
|
||||||
sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint, "unit": location.temperatureScale)
|
sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint)
|
||||||
sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint, "unit": location.temperatureScale)
|
sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint)
|
||||||
log.debug "alterSetpoint in mode $mode succeed change setpoint to= ${temp.value}"
|
log.debug "alterSetpoint in mode $mode succeed change setpoint to= ${temp.value}"
|
||||||
} else {
|
} else {
|
||||||
log.error "Error alterSetpoint()"
|
log.error "Error alterSetpoint()"
|
||||||
|
|||||||
@@ -682,7 +682,7 @@ def setHeatingSetpoint(degrees) {
|
|||||||
def temperatureScale = getTemperatureScale()
|
def temperatureScale = getTemperatureScale()
|
||||||
|
|
||||||
def degreesInteger = degrees as Integer
|
def degreesInteger = degrees as Integer
|
||||||
sendEvent("name":"heatingSetpoint", "value":degreesInteger, "unit":temperatureScale)
|
sendEvent("name":"heatingSetpoint", "value":degreesInteger)
|
||||||
|
|
||||||
def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
|
def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
|
||||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius*100) + "}"
|
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius*100) + "}"
|
||||||
@@ -691,7 +691,7 @@ def setHeatingSetpoint(degrees) {
|
|||||||
|
|
||||||
def setCoolingSetpoint(degrees) {
|
def setCoolingSetpoint(degrees) {
|
||||||
def degreesInteger = degrees as Integer
|
def degreesInteger = degrees as Integer
|
||||||
sendEvent("name":"coolingSetpoint", "value":degreesInteger, "unit":temperatureScale)
|
sendEvent("name":"coolingSetpoint", "value":degreesInteger)
|
||||||
def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
|
def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
|
||||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius*100) + "}"
|
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius*100) + "}"
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
.st-ignore
|
|
||||||
README.md
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
## 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)
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
.st-ignore
|
|
||||||
README.md
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
## 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)
|
|
||||||
@@ -259,8 +259,7 @@ private Map getTemperatureResult(value) {
|
|||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText,
|
descriptionText: descriptionText,
|
||||||
translatable: true,
|
translatable: true
|
||||||
unit: temperatureScale
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ Works with:
|
|||||||
## Table of contents
|
## Table of contents
|
||||||
|
|
||||||
* [Capabilities](#capabilities)
|
* [Capabilities](#capabilities)
|
||||||
* [Health](#device-health)
|
* [Health]($health)
|
||||||
* [Battery](#battery-specification)
|
|
||||||
|
|
||||||
## Capabilities
|
## Capabilities
|
||||||
|
|
||||||
@@ -22,24 +21,10 @@ Works with:
|
|||||||
|
|
||||||
## Device Health
|
## Device Health
|
||||||
|
|
||||||
A Category C2 motion sensor with maxReportTime of 1 hr.
|
A Category C2 motion sensor that has 120min check-in interval
|
||||||
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 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)
|
|
||||||
|
|||||||
@@ -274,8 +274,7 @@ private Map getTemperatureResult(value) {
|
|||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText,
|
descriptionText: descriptionText,
|
||||||
translatable: true,
|
translatable: true
|
||||||
unit: temperatureScale
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -226,8 +226,7 @@ private Map getTemperatureResult(value) {
|
|||||||
return [
|
return [
|
||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText,
|
descriptionText: descriptionText
|
||||||
unit: temperatureScale
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
.st-ignore
|
|
||||||
README.md
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
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)
|
|
||||||
@@ -333,11 +333,10 @@ private Map getTemperatureResult(value) {
|
|||||||
'{{ device.displayName }} was {{ value }}°F'
|
'{{ device.displayName }} was {{ value }}°F'
|
||||||
|
|
||||||
return [
|
return [
|
||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText,
|
descriptionText: descriptionText,
|
||||||
translatable: true,
|
translatable: true
|
||||||
unit: temperatureScale
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -223,10 +223,9 @@ def getTemperature(value) {
|
|||||||
}
|
}
|
||||||
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
|
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
|
||||||
return [
|
return [
|
||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText,
|
descriptionText: descriptionText
|
||||||
unit: temperatureScale
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
.st-ignore
|
|
||||||
README.md
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
## 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)
|
|
||||||
@@ -226,8 +226,7 @@ private Map getTemperatureResult(value) {
|
|||||||
return [
|
return [
|
||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText,
|
descriptionText: descriptionText
|
||||||
unit: temperatureScale
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
.st-ignore
|
|
||||||
README.md
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
## 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)
|
|
||||||
@@ -233,8 +233,7 @@ private Map getTemperatureResult(value) {
|
|||||||
return [
|
return [
|
||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText,
|
descriptionText: descriptionText
|
||||||
unit: temperatureScale
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -213,8 +213,7 @@ private Map getTemperatureResult(value) {
|
|||||||
return [
|
return [
|
||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText,
|
descriptionText: descriptionText
|
||||||
unit: temperatureScale
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ 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: "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: "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: "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
|
// UI tile definitions
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
/**
|
|
||||||
* JSON
|
|
||||||
*
|
|
||||||
* Copyright 2015 Jesse Newland
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
|
||||||
* in compliance with the License. You may obtain a copy of the License at:
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
|
||||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
|
||||||
* for the specific language governing permissions and limitations under the License.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
definition(
|
|
||||||
name: "JSON API",
|
|
||||||
namespace: "jnewland",
|
|
||||||
author: "Jesse Newland",
|
|
||||||
description: "A JSON API for SmartThings",
|
|
||||||
category: "SmartThings Labs",
|
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
|
||||||
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
|
||||||
oauth: true)
|
|
||||||
|
|
||||||
|
|
||||||
def installed() {
|
|
||||||
initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
def updated() {
|
|
||||||
unsubscribe()
|
|
||||||
initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
def initialize() {
|
|
||||||
if (!state.accessToken) {
|
|
||||||
createAccessToken()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
preferences {
|
|
||||||
page(name: "copyConfig")
|
|
||||||
}
|
|
||||||
|
|
||||||
def copyConfig() {
|
|
||||||
if (!state.accessToken) {
|
|
||||||
createAccessToken()
|
|
||||||
}
|
|
||||||
dynamicPage(name: "copyConfig", title: "Config", install:true) {
|
|
||||||
section("Select devices to include in the /devices API call") {
|
|
||||||
input "switches", "capability.switch", title: "Switches", multiple: true, required: false
|
|
||||||
input "hues", "capability.colorControl", title: "Hues", multiple: true, required: false
|
|
||||||
}
|
|
||||||
|
|
||||||
section() {
|
|
||||||
paragraph "View this SmartApp's configuration to use it in other places."
|
|
||||||
href url:"https://graph-eu01-euwest1.api.smartthings.com/api/smartapps/installations/${app.id}/config?access_token=${state.accessToken}", style:"embedded", required:false, title:"Config", description:"Tap, select, copy, then click \"Done\""
|
|
||||||
}
|
|
||||||
|
|
||||||
section() {
|
|
||||||
href url:"https://graph-eu01-euwest1.api.smartthings.com/api/smartapps/installations/${app.id}/devices?access_token=${state.accessToken}", style:"embedded", required:false, title:"Debug", description:"View accessories JSON"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def renderConfig() {
|
|
||||||
def configJson = new groovy.json.JsonOutput().toJson([
|
|
||||||
description: "JSON API",
|
|
||||||
platforms: [
|
|
||||||
[
|
|
||||||
platform: "SmartThings",
|
|
||||||
name: "SmartThings",
|
|
||||||
app_id: app.id,
|
|
||||||
access_token: state.accessToken
|
|
||||||
]
|
|
||||||
],
|
|
||||||
])
|
|
||||||
|
|
||||||
def configString = new groovy.json.JsonOutput().prettyPrint(configJson)
|
|
||||||
render contentType: "text/plain", data: configString
|
|
||||||
}
|
|
||||||
|
|
||||||
def deviceCommandMap(device, type) {
|
|
||||||
device.supportedCommands.collectEntries { command->
|
|
||||||
def commandUrl = "https://graph-eu01-euwest1.api.smartthings.com/api/smartapps/installations/${app.id}/${type}/${device.id}/command/${command.name}?access_token=${state.accessToken}"
|
|
||||||
[
|
|
||||||
(command.name): commandUrl
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def authorizedDevices() {
|
|
||||||
[
|
|
||||||
switches: switches,
|
|
||||||
hues: hues
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
def renderDevices() {
|
|
||||||
def deviceData = authorizedDevices().collectEntries { devices->
|
|
||||||
[
|
|
||||||
(devices.key): devices.value.collect { device->
|
|
||||||
[
|
|
||||||
name: device.displayName,
|
|
||||||
commands: deviceCommandMap(device, devices.key)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
def deviceJson = new groovy.json.JsonOutput().toJson(deviceData)
|
|
||||||
def deviceString = new groovy.json.JsonOutput().prettyPrint(deviceJson)
|
|
||||||
render contentType: "application/json", data: deviceString
|
|
||||||
}
|
|
||||||
|
|
||||||
def deviceCommand() {
|
|
||||||
def device = authorizedDevices()[params.type].find { it.id == params.id }
|
|
||||||
def command = params.command
|
|
||||||
if (!device) {
|
|
||||||
httpError(404, "Device not found")
|
|
||||||
} else {
|
|
||||||
if (params.value) {
|
|
||||||
device."$command"(params.value)
|
|
||||||
} else {
|
|
||||||
device."$command"()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mappings {
|
|
||||||
if (!params.access_token || (params.access_token && params.access_token != state.accessToken)) {
|
|
||||||
path("/devices") { action: [GET: "authError"] }
|
|
||||||
path("/config") { action: [GET: "authError"] }
|
|
||||||
path("/:type/:id/command/:command") { action: [PUT: "authError"] }
|
|
||||||
} else {
|
|
||||||
path("/devices") { action: [GET: "renderDevices"] }
|
|
||||||
path("/config") { action: [GET: "renderConfig"] }
|
|
||||||
path("/:type/:id/command/:command") { action: [PUT: "deviceCommand"] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def authError() {
|
|
||||||
[error: "Permission denied"]
|
|
||||||
}
|
|
||||||
@@ -62,7 +62,7 @@ def initialize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def sendit(evt) {
|
def sendit(evt) {
|
||||||
log.debug "$evt.value: $evt"
|
log.debug "$evt.value: $evt, $settings"
|
||||||
sendMessage()
|
sendMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +80,6 @@ def sendMessage() {
|
|||||||
sendSms phone3, msg
|
sendSms phone3, msg
|
||||||
}
|
}
|
||||||
if (!phone1 && !phone2 && !phone3) {
|
if (!phone1 && !phone2 && !phone3) {
|
||||||
sendPush msg
|
sendPush msg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ def authPage() {
|
|||||||
// get rid of next button until the user is actually auth'd
|
// get rid of next button until the user is actually auth'd
|
||||||
if (!oauthTokenProvided) {
|
if (!oauthTokenProvided) {
|
||||||
return dynamicPage(name: "auth", title: "Login", nextPage: "", uninstall:uninstallAllowed) {
|
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."
|
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
|
href url:redirectUrl, style:"embedded", required:true, title:"ecobee", description:description
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ def authPage() {
|
|||||||
log.debug "thermostat list: $stats"
|
log.debug "thermostat list: $stats"
|
||||||
log.debug "sensor list: ${sensorsDiscovered()}"
|
log.debug "sensor list: ${sensorsDiscovered()}"
|
||||||
return dynamicPage(name: "auth", title: "Select Your Thermostats", uninstall: true) {
|
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."
|
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])
|
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 options = sensorsDiscovered() ?: []
|
||||||
def numFound = options.size() ?: 0
|
def numFound = options.size() ?: 0
|
||||||
if (numFound > 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."
|
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)
|
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 code = params.code
|
||||||
def oauthState = params.state
|
def oauthState = params.state
|
||||||
|
|
||||||
if (oauthState == atomicState.oauthInitState) {
|
if (oauthState == atomicState.oauthInitState){
|
||||||
|
|
||||||
def tokenParams = [
|
def tokenParams = [
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code : code,
|
code : code,
|
||||||
client_id : smartThingsClientId,
|
client_id : smartThingsClientId,
|
||||||
redirect_uri: callbackUrl
|
redirect_uri: callbackUrl
|
||||||
]
|
]
|
||||||
|
|
||||||
def tokenUrl = "https://www.ecobee.com/home/token?${toQueryString(tokenParams)}"
|
def tokenUrl = "https://www.ecobee.com/home/token?${toQueryString(tokenParams)}"
|
||||||
@@ -128,6 +129,9 @@ def callback() {
|
|||||||
httpPost(uri: tokenUrl) { resp ->
|
httpPost(uri: tokenUrl) { resp ->
|
||||||
atomicState.refreshToken = resp.data.refresh_token
|
atomicState.refreshToken = resp.data.refresh_token
|
||||||
atomicState.authToken = resp.data.access_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) {
|
if (atomicState.authToken) {
|
||||||
@@ -144,8 +148,8 @@ def callback() {
|
|||||||
|
|
||||||
def success() {
|
def success() {
|
||||||
def message = """
|
def message = """
|
||||||
<p>Your ecobee Account is now connected to SmartThings!</p>
|
<p>Your ecobee Account is now connected to SmartThings!</p>
|
||||||
<p>Click 'Done' to finish setup.</p>
|
<p>Click 'Done' to finish setup.</p>
|
||||||
"""
|
"""
|
||||||
connectionStatus(message)
|
connectionStatus(message)
|
||||||
}
|
}
|
||||||
@@ -167,63 +171,64 @@ def connectionStatus(message, redirectUrl = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def html = """
|
def html = """
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=640">
|
<meta name="viewport" content="width=640">
|
||||||
<title>Ecobee & SmartThings connection</title>
|
<title>Ecobee & SmartThings connection</title>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Swiss 721 W01 Thin';
|
font-family: 'Swiss 721 W01 Thin';
|
||||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
|
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
|
||||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
|
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
|
||||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
|
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
|
||||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
|
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
|
||||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
|
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Swiss 721 W01 Light';
|
font-family: 'Swiss 721 W01 Light';
|
||||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
|
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
|
||||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
|
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
|
||||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
|
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
|
||||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
|
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
|
||||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
|
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
padding: 4%;
|
padding: 4%;
|
||||||
text-align: center;
|
/*background: #eee;*/
|
||||||
}
|
text-align: center;
|
||||||
img {
|
}
|
||||||
vertical-align: middle;
|
img {
|
||||||
}
|
vertical-align: middle;
|
||||||
p {
|
}
|
||||||
font-size: 2.2em;
|
p {
|
||||||
font-family: 'Swiss 721 W01 Thin';
|
font-size: 2.2em;
|
||||||
text-align: center;
|
font-family: 'Swiss 721 W01 Thin';
|
||||||
color: #666666;
|
text-align: center;
|
||||||
padding: 0 40px;
|
color: #666666;
|
||||||
margin-bottom: 0;
|
padding: 0 40px;
|
||||||
}
|
margin-bottom: 0;
|
||||||
span {
|
}
|
||||||
font-family: 'Swiss 721 W01 Light';
|
span {
|
||||||
}
|
font-family: 'Swiss 721 W01 Light';
|
||||||
</style>
|
}
|
||||||
</head>
|
</style>
|
||||||
<body>
|
</head>
|
||||||
<div class="container">
|
<body>
|
||||||
|
<div class="container">
|
||||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/ecobee%402x.png" alt="ecobee icon" />
|
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/ecobee%402x.png" alt="ecobee icon" />
|
||||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
|
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
|
||||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
|
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
|
||||||
${message}
|
${message}
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
render contentType: 'text/html', data: html
|
render contentType: 'text/html', data: html
|
||||||
}
|
}
|
||||||
@@ -232,26 +237,19 @@ def getEcobeeThermostats() {
|
|||||||
log.debug "getting device list"
|
log.debug "getting device list"
|
||||||
atomicState.remoteSensors = []
|
atomicState.remoteSensors = []
|
||||||
|
|
||||||
def bodyParams = [
|
def requestBody = '{"selection":{"selectionType":"registered","selectionMatch":"","includeRuntime":true,"includeSensors":true}}'
|
||||||
selection: [
|
|
||||||
selectionType: "registered",
|
|
||||||
selectionMatch: "",
|
|
||||||
includeRuntime: true,
|
|
||||||
includeSensors: true
|
|
||||||
]
|
|
||||||
]
|
|
||||||
def deviceListParams = [
|
def deviceListParams = [
|
||||||
uri: apiEndpoint,
|
uri: apiEndpoint,
|
||||||
path: "/1/thermostat",
|
path: "/1/thermostat",
|
||||||
headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
||||||
// TODO - the query string below is not consistent with the Ecobee docs:
|
query: [format: 'json', body: requestBody]
|
||||||
// https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml
|
|
||||||
query: [format: 'json', body: toJson(bodyParams)]
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def stats = [:]
|
def stats = [:]
|
||||||
try {
|
try {
|
||||||
httpGet(deviceListParams) { resp ->
|
httpGet(deviceListParams) { resp ->
|
||||||
|
|
||||||
if (resp.status == 200) {
|
if (resp.status == 200) {
|
||||||
resp.data.thermostatList.each { stat ->
|
resp.data.thermostatList.each { stat ->
|
||||||
atomicState.remoteSensors = atomicState.remoteSensors == null ? stat.remoteSensors : atomicState.remoteSensors << stat.remoteSensors
|
atomicState.remoteSensors = atomicState.remoteSensors == null ? stat.remoteSensors : atomicState.remoteSensors << stat.remoteSensors
|
||||||
@@ -291,10 +289,9 @@ Map sensorsDiscovered() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def getThermostatDisplayName(stat) {
|
def getThermostatDisplayName(stat) {
|
||||||
if(stat?.name) {
|
if(stat?.name)
|
||||||
return stat.name.toString()
|
return stat.name.toString()
|
||||||
}
|
return (getThermostatTypeName(stat) + " (${stat.identifier})").toString()
|
||||||
return (getThermostatTypeName(stat) + " (${stat.identifier})").toString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def getThermostatTypeName(stat) {
|
def getThermostatTypeName(stat) {
|
||||||
@@ -313,6 +310,7 @@ def updated() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def initialize() {
|
def initialize() {
|
||||||
|
|
||||||
log.debug "initialize"
|
log.debug "initialize"
|
||||||
def devices = thermostats.collect { dni ->
|
def devices = thermostats.collect { dni ->
|
||||||
def d = getChildDevice(dni)
|
def d = getChildDevice(dni)
|
||||||
@@ -352,6 +350,8 @@ def initialize() {
|
|||||||
log.warn "delete: ${delete}, deleting ${delete.size()} thermostats"
|
log.warn "delete: ${delete}, deleting ${delete.size()} thermostats"
|
||||||
delete.each { deleteChildDevice(it.deviceNetworkId) } //inherits from SmartApp (data-management)
|
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
|
//send activity feeds to tell that device is connected
|
||||||
def notificationMessage = "is connected to SmartThings"
|
def notificationMessage = "is connected to SmartThings"
|
||||||
sendActivityFeeds(notificationMessage)
|
sendActivityFeeds(notificationMessage)
|
||||||
@@ -381,41 +381,75 @@ def pollHandler() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def pollChildren(child = null) {
|
def pollChildren(child = null) {
|
||||||
def thermostatIdsString = getChildDeviceIdsString()
|
def thermostatIdsString = getChildDeviceIdsString()
|
||||||
log.debug "polling children: $thermostatIdsString"
|
log.debug "polling children: $thermostatIdsString"
|
||||||
|
def data = ""
|
||||||
def requestBody = [
|
|
||||||
selection: [
|
|
||||||
selectionType: "thermostats",
|
|
||||||
selectionMatch: thermostatIdsString,
|
|
||||||
includeExtendedRuntime: true,
|
|
||||||
includeSettings: true,
|
|
||||||
includeRuntime: true,
|
|
||||||
includeSensors: true
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeExtendedRuntime":"true","includeSettings":"true","includeRuntime":"true","includeSensors":true}}'
|
||||||
def result = false
|
def result = false
|
||||||
|
|
||||||
def pollParams = [
|
def pollParams = [
|
||||||
uri: apiEndpoint,
|
uri: apiEndpoint,
|
||||||
path: "/1/thermostat",
|
path: "/1/thermostat",
|
||||||
headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
||||||
// TODO - the query string below is not consistent with the Ecobee docs:
|
query: [format: 'json', body: jsonRequestBody]
|
||||||
// https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml
|
]
|
||||||
query: [format: 'json', body: toJson(requestBody)]
|
|
||||||
]
|
|
||||||
|
|
||||||
try{
|
try{
|
||||||
httpGet(pollParams) { resp ->
|
httpGet(pollParams) { resp ->
|
||||||
if(resp.status == 200) {
|
if(resp.status == 200) {
|
||||||
log.debug "poll results returned resp.data ${resp.data}"
|
log.debug "poll results returned resp.data ${resp.data}"
|
||||||
atomicState.remoteSensors = resp.data.thermostatList.remoteSensors
|
atomicState.remoteSensors = resp.data.thermostatList.remoteSensors
|
||||||
updateSensorData()
|
atomicState.thermostatData = resp.data
|
||||||
storeThermostatData(resp.data.thermostatList)
|
updateSensorData()
|
||||||
result = true
|
atomicState.thermostats = resp.data.thermostatList.inject([:]) { collector, stat ->
|
||||||
log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}"
|
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) {
|
} catch (groovyx.net.http.HttpResponseException e) {
|
||||||
log.trace "Exception polling children: " + e.response.data.status
|
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
|
// Poll Child is invoked from the Child Device itself as part of the Poll Capability
|
||||||
def pollChild() {
|
def pollChild(){
|
||||||
|
|
||||||
def devices = getChildDevices()
|
def devices = getChildDevices()
|
||||||
|
|
||||||
if (pollChildren()) {
|
if (pollChildren()){
|
||||||
devices.each { child ->
|
devices.each { child ->
|
||||||
if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")) {
|
if (!child.device.deviceNetworkId.startsWith("ecobee_sensor")){
|
||||||
if(atomicState.thermostats[child.device.deviceNetworkId] != null) {
|
if(atomicState.thermostats[child.device.deviceNetworkId] != null) {
|
||||||
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
||||||
log.info "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}"
|
log.info "pollChild(child)>> data for ${child.device.deviceNetworkId} : ${tData.data}"
|
||||||
@@ -457,38 +492,36 @@ void poll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def availableModes(child) {
|
def availableModes(child) {
|
||||||
|
|
||||||
debugEvent ("atomicState.thermostats = ${atomicState.thermostats}")
|
debugEvent ("atomicState.thermostats = ${atomicState.thermostats}")
|
||||||
|
|
||||||
debugEvent ("Child DNI = ${child.device.deviceNetworkId}")
|
debugEvent ("Child DNI = ${child.device.deviceNetworkId}")
|
||||||
|
|
||||||
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
||||||
|
|
||||||
debugEvent("Data = ${tData}")
|
debugEvent("Data = ${tData}")
|
||||||
|
|
||||||
if(!tData) {
|
if(!tData)
|
||||||
|
{
|
||||||
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling"
|
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling"
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
def modes = ["off"]
|
def modes = ["off"]
|
||||||
|
|
||||||
if (tData.data.heatMode) {
|
if (tData.data.heatMode) modes.add("heat")
|
||||||
modes.add("heat")
|
if (tData.data.coolMode) modes.add("cool")
|
||||||
}
|
if (tData.data.autoMode) modes.add("auto")
|
||||||
if (tData.data.coolMode) {
|
if (tData.data.auxHeatMode) modes.add("auxHeatOnly")
|
||||||
modes.add("cool")
|
|
||||||
}
|
modes
|
||||||
if (tData.data.autoMode) {
|
|
||||||
modes.add("auto")
|
|
||||||
}
|
|
||||||
if (tData.data.auxHeatMode) {
|
|
||||||
modes.add("auxHeatOnly")
|
|
||||||
}
|
|
||||||
|
|
||||||
return modes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def currentMode(child) {
|
def currentMode(child) {
|
||||||
debugEvent ("atomicState.Thermos = ${atomicState.thermostats}")
|
debugEvent ("atomicState.Thermos = ${atomicState.thermostats}")
|
||||||
|
|
||||||
debugEvent ("Child DNI = ${child.device.deviceNetworkId}")
|
debugEvent ("Child DNI = ${child.device.deviceNetworkId}")
|
||||||
|
|
||||||
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
||||||
@@ -497,11 +530,14 @@ def currentMode(child) {
|
|||||||
|
|
||||||
if(!tData) {
|
if(!tData) {
|
||||||
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling"
|
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling"
|
||||||
|
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
def mode = tData.data.thermostatMode
|
def mode = tData.data.thermostatMode
|
||||||
return mode
|
|
||||||
|
mode
|
||||||
}
|
}
|
||||||
|
|
||||||
def updateSensorData() {
|
def updateSensorData() {
|
||||||
@@ -522,12 +558,12 @@ def updateSensorData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (it.type == "occupancy") {
|
} else if (it.type == "occupancy") {
|
||||||
if(it.value == "true") {
|
if(it.value == "true")
|
||||||
occupancy = "active"
|
occupancy = "active"
|
||||||
} else {
|
else
|
||||||
occupancy = "inactive"
|
occupancy = "inactive"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
def dni = "ecobee_sensor-"+ it?.id + "-" + it?.code
|
def dni = "ecobee_sensor-"+ it?.id + "-" + it?.code
|
||||||
@@ -546,7 +582,7 @@ def getChildDeviceIdsString() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def toJson(Map m) {
|
def toJson(Map m) {
|
||||||
return groovy.json.JsonOutput.toJson(m)
|
return new org.json.JSONObject(m).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
def toQueryString(Map m) {
|
def toQueryString(Map m) {
|
||||||
@@ -559,24 +595,54 @@ private refreshAuthToken() {
|
|||||||
if(!atomicState.refreshToken) {
|
if(!atomicState.refreshToken) {
|
||||||
log.warn "Can not refresh OAuth token since there is no refreshToken stored"
|
log.warn "Can not refresh OAuth token since there is no refreshToken stored"
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
def refreshParams = [
|
def refreshParams = [
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
uri : apiEndpoint,
|
uri : apiEndpoint,
|
||||||
path : "/token",
|
path : "/token",
|
||||||
query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId],
|
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."
|
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
|
//changed to httpPost
|
||||||
try {
|
try {
|
||||||
def jsonMap
|
def jsonMap
|
||||||
httpPost(refreshParams) { resp ->
|
httpPost(refreshParams) { resp ->
|
||||||
|
|
||||||
if(resp.status == 200) {
|
if(resp.status == 200) {
|
||||||
log.debug "Token refreshed...calling saved RestAction now!"
|
log.debug "Token refreshed...calling saved RestAction now!"
|
||||||
|
|
||||||
debugEvent("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) {
|
} catch (groovyx.net.http.HttpResponseException e) {
|
||||||
log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}"
|
log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}"
|
||||||
def reAttemptPeriod = 300 // in sec
|
def reAttemptPeriod = 300 // in sec
|
||||||
@@ -596,220 +662,118 @@ private refreshAuthToken() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
def resumeProgram(child, deviceId) {
|
||||||
* 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.
|
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}]}'
|
||||||
*
|
def result = sendJson(jsonRequestBody)
|
||||||
* @param json - an object representing the parsed JSON response from Ecobee
|
return result
|
||||||
*/
|
|
||||||
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 setHold(child, heating, cooling, deviceId, sendHoldType) {
|
||||||
* Executes the resume program command on the Ecobee thermostat
|
|
||||||
* @param deviceId - the ID of the device
|
int h = heating * 10
|
||||||
*
|
int c = cooling * 10
|
||||||
* @retrun true if the command was successful, false otherwise.
|
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": '+sendHoldType+' } } ]}'
|
||||||
*/
|
|
||||||
boolean resumeProgram(deviceId) {
|
def result = sendJson(child, jsonRequestBody)
|
||||||
def payload = [
|
return result
|
||||||
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
|
|
||||||
|
|
||||||
def payload = [
|
int h = heating * 10
|
||||||
selection: [
|
int c = cooling * 10
|
||||||
selectionType: "thermostats",
|
|
||||||
selectionMatch: deviceId,
|
|
||||||
includeRuntime: true
|
|
||||||
],
|
|
||||||
functions: [
|
|
||||||
[
|
|
||||||
type: "setHold",
|
|
||||||
params: [
|
|
||||||
coolHoldTemp: c,
|
|
||||||
heatHoldTemp: h,
|
|
||||||
holdType: sendHoldType
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
def setMode(child, mode, deviceId) {
|
||||||
* Executes the set fan mode command on the Ecobee thermostat
|
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"thermostat": {"settings":{"hvacMode":"'+"${mode}"+'"}}}'
|
||||||
* @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 payload = [
|
def result = sendJson(jsonRequestBody)
|
||||||
selection: [
|
return result
|
||||||
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 = [
|
def cmdParams = [
|
||||||
uri: apiEndpoint,
|
uri: apiEndpoint,
|
||||||
path: "/1/thermostat",
|
path: "/1/thermostat",
|
||||||
headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
||||||
body: toJson(bodyParams)
|
body: jsonBody
|
||||||
]
|
]
|
||||||
|
|
||||||
try{
|
try{
|
||||||
httpPost(cmdParams) { resp ->
|
httpPost(cmdParams) { resp ->
|
||||||
if(resp.status == 200) {
|
|
||||||
log.debug "updated ${resp.data}"
|
if(resp.status == 200) {
|
||||||
def returnStatus = resp.data.status.code
|
|
||||||
if (returnStatus == 0) {
|
log.debug "updated ${resp.data}"
|
||||||
log.debug "Successful call to ecobee API."
|
returnStatus = resp.data.status.code
|
||||||
isSuccess = true
|
if (resp.data.status.code == 0)
|
||||||
} else {
|
log.debug "Successful call to ecobee API."
|
||||||
log.debug "Error return code = ${returnStatus}"
|
else {
|
||||||
debugEvent("Error return code = ${returnStatus}")
|
log.debug "Error return code = ${resp.data.status.code}"
|
||||||
}
|
debugEvent("Error return code = ${resp.data.status.code}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (groovyx.net.http.HttpResponseException e) {
|
} catch (groovyx.net.http.HttpResponseException e) {
|
||||||
log.trace "Exception Sending Json: " + e.response.data.status
|
log.trace "Exception Sending Json: " + e.response.data.status
|
||||||
debugEvent ("sent Json & got http status ${e.statusCode} - ${e.response.data.status.code}")
|
debugEvent ("sent Json & got http status ${e.statusCode} - ${e.response.data.status.code}")
|
||||||
if (e.response.data.status.code == 14) {
|
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"
|
atomicState.action = "pollChildren"
|
||||||
log.debug "Refreshing your auth_token!"
|
log.debug "Refreshing your auth_token!"
|
||||||
refreshAuthToken()
|
refreshAuthToken()
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
debugEvent("Authentication error, invalid authentication method, lack of credentials, etc.")
|
debugEvent("Authentication error, invalid authentication method, lack of credentials, etc.")
|
||||||
log.error "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 getChildName() { "Ecobee Thermostat" }
|
||||||
def getSensorChildName() { return "Ecobee Sensor" }
|
def getSensorChildName() { "Ecobee Sensor" }
|
||||||
def getServerUrl() { return "https://graph.api.smartthings.com" }
|
def getServerUrl() { return "https://graph.api.smartthings.com" }
|
||||||
def getShardUrl() { return getApiServerUrl() }
|
def getShardUrl() { return getApiServerUrl() }
|
||||||
def getCallbackUrl() { return "https://graph.api.smartthings.com/oauth/callback" }
|
def getCallbackUrl() { "https://graph.api.smartthings.com/oauth/callback" }
|
||||||
def getBuildRedirectUrl() { return "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${shardUrl}" }
|
def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${shardUrl}" }
|
||||||
def getApiEndpoint() { return "https://api.ecobee.com" }
|
def getApiEndpoint() { "https://api.ecobee.com" }
|
||||||
def getSmartThingsClientId() { return appSettings.clientId }
|
def getSmartThingsClientId() { appSettings.clientId }
|
||||||
|
|
||||||
def debugEvent(message, displayEvent = false) {
|
def debugEvent(message, displayEvent = false) {
|
||||||
|
|
||||||
def results = [
|
def results = [
|
||||||
name: "appdebug",
|
name: "appdebug",
|
||||||
descriptionText: message,
|
descriptionText: message,
|
||||||
displayed: displayEvent
|
displayed: displayEvent
|
||||||
]
|
]
|
||||||
log.debug "Generating AppDebug Event: ${results}"
|
log.debug "Generating AppDebug Event: ${results}"
|
||||||
sendEvent (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
|
//send both push notification and mobile activity feeds
|
||||||
def sendPushAndFeeds(notificationMessage) {
|
def sendPushAndFeeds(notificationMessage){
|
||||||
log.warn "sendPushAndFeeds >> notificationMessage: ${notificationMessage}"
|
log.warn "sendPushAndFeeds >> notificationMessage: ${notificationMessage}"
|
||||||
log.warn "sendPushAndFeeds >> atomicState.timeSendPush: ${atomicState.timeSendPush}"
|
log.warn "sendPushAndFeeds >> atomicState.timeSendPush: ${atomicState.timeSendPush}"
|
||||||
if (atomicState.timeSendPush) {
|
if (atomicState.timeSendPush){
|
||||||
if (now() - atomicState.timeSendPush > 86400000) { // notification is sent to remind user once a day
|
if (now() - atomicState.timeSendPush > 86400000){ // notification is sent to remind user once a day
|
||||||
sendPush("Your Ecobee thermostat " + notificationMessage)
|
sendPush("Your Ecobee thermostat " + notificationMessage)
|
||||||
sendActivityFeeds(notificationMessage)
|
sendActivityFeeds(notificationMessage)
|
||||||
atomicState.timeSendPush = now()
|
atomicState.timeSendPush = now()
|
||||||
@@ -822,58 +786,6 @@ def sendPushAndFeeds(notificationMessage) {
|
|||||||
atomicState.authToken = null
|
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 sendActivityFeeds(notificationMessage) {
|
||||||
def devices = getChildDevices()
|
def devices = getChildDevices()
|
||||||
devices.each { child ->
|
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) {
|
def convertFtoC (tempF) {
|
||||||
return String.format("%.1f", (Math.round(((tempF - 32)*(5/9)) * 2))/2)
|
return String.format("%.1f", (Math.round(((tempF - 32)*(5/9)) * 2))/2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def convertCtoF (tempC) {
|
||||||
|
return (Math.round(tempC * (9/5)) + 32).toInteger()
|
||||||
|
}
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ def authPage()
|
|||||||
|
|
||||||
def redirectUrl = oauthInitUrl()
|
def redirectUrl = oauthInitUrl()
|
||||||
|
|
||||||
|
log.debug "RedirectURL = ${redirectUrl}"
|
||||||
|
|
||||||
return dynamicPage(name: "Credentials", title: "Life360", nextPage:"listCirclesPage", uninstall: uninstallOption, install:false) {
|
return dynamicPage(name: "Credentials", title: "Life360", nextPage:"listCirclesPage", uninstall: uninstallOption, install:false) {
|
||||||
section {
|
section {
|
||||||
href url:redirectUrl, style:"embedded", required:false, title:"Life360", description:description
|
href url:redirectUrl, style:"embedded", required:false, title:"Life360", description:description
|
||||||
@@ -255,6 +257,8 @@ def initializeLife360Connection() {
|
|||||||
def oauthClientId = appSettings.clientId
|
def oauthClientId = appSettings.clientId
|
||||||
def oauthClientSecret = appSettings.clientSecret
|
def oauthClientSecret = appSettings.clientSecret
|
||||||
|
|
||||||
|
log.debug "Installed with settings: ${settings}"
|
||||||
|
|
||||||
initialize()
|
initialize()
|
||||||
|
|
||||||
def username = settings.username
|
def username = settings.username
|
||||||
@@ -265,6 +269,8 @@ def initializeLife360Connection() {
|
|||||||
def basicCredentials = "${oauthClientId}:${oauthClientSecret}"
|
def basicCredentials = "${oauthClientId}:${oauthClientSecret}"
|
||||||
def encodedCredentials = basicCredentials.encodeAsBase64().toString()
|
def encodedCredentials = basicCredentials.encodeAsBase64().toString()
|
||||||
|
|
||||||
|
log.debug "Encoded Creds: ${encodedCredentials}"
|
||||||
|
|
||||||
|
|
||||||
// call life360, get OAUTH token using password flow, save
|
// call life360, get OAUTH token using password flow, save
|
||||||
// curl -X POST -H "Authorization: Basic cFJFcXVnYWJSZXRyZTRFc3RldGhlcnVmcmVQdW1hbUV4dWNyRUh1YzptM2ZydXBSZXRSZXN3ZXJFQ2hBUHJFOTZxYWtFZHI0Vg=="
|
// curl -X POST -H "Authorization: Basic cFJFcXVnYWJSZXRyZTRFc3RldGhlcnVmcmVQdW1hbUV4dWNyRUh1YzptM2ZydXBSZXRSZXN3ZXJFQ2hBUHJFOTZxYWtFZHI0Vg=="
|
||||||
@@ -278,6 +284,8 @@ def initializeLife360Connection() {
|
|||||||
"username=${username}&"+
|
"username=${username}&"+
|
||||||
"password=${password}"
|
"password=${password}"
|
||||||
|
|
||||||
|
log.debug "Post Body: ${postBody}"
|
||||||
|
|
||||||
def result = null
|
def result = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -287,6 +295,7 @@ def initializeLife360Connection() {
|
|||||||
}
|
}
|
||||||
if (result.data.access_token) {
|
if (result.data.access_token) {
|
||||||
state.life360AccessToken = result.data.access_token
|
state.life360AccessToken = result.data.access_token
|
||||||
|
log.debug "Access Token = ${state.life360AccessToken}"
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
log.debug "Response=${result.data}"
|
log.debug "Response=${result.data}"
|
||||||
@@ -524,6 +533,8 @@ def createCircleSubscription() {
|
|||||||
|
|
||||||
def postBody = "url=${hookUrl}"
|
def postBody = "url=${hookUrl}"
|
||||||
|
|
||||||
|
log.debug "Post Body: ${postBody}"
|
||||||
|
|
||||||
def result = null
|
def result = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -575,6 +586,8 @@ def updated() {
|
|||||||
|
|
||||||
// log.debug "After Find Attempt."
|
// 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}"
|
// log.debug "External Id=${app.id}:${member.id}"
|
||||||
|
|
||||||
// create the device
|
// create the device
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ preferences {
|
|||||||
}
|
}
|
||||||
section("Via a push notification and/or an SMS message"){
|
section("Via a push notification and/or an SMS message"){
|
||||||
input("recipients", "contact", title: "Send notifications to") {
|
input("recipients", "contact", title: "Send notifications to") {
|
||||||
input "phone", "phone", title: "Enter a phone number to get SMS", required: false
|
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"
|
paragraph "If outside the US please make sure to enter the proper country code"
|
||||||
input "pushAndPhone", "enum", title: "Notify me via Push Notification", required: false, options: ["Yes", "No"]
|
input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes", "No"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
section("Minimum time between messages (optional, defaults to every message)") {
|
section("Minimum time between messages (optional, defaults to every message)") {
|
||||||
@@ -111,17 +111,19 @@ private sendMessage(evt) {
|
|||||||
if (location.contactBookEnabled) {
|
if (location.contactBookEnabled) {
|
||||||
sendNotificationToContacts(msg, recipients, options)
|
sendNotificationToContacts(msg, recipients, options)
|
||||||
} else {
|
} else {
|
||||||
if (pushAndPhone != 'No') {
|
if (!phone || pushAndPhone != 'No') {
|
||||||
log.debug 'sending push'
|
log.debug 'sending push'
|
||||||
options.method = 'push'
|
options.method = 'push'
|
||||||
sendNotification(msg, options)
|
//sendPush(msg)
|
||||||
}
|
}
|
||||||
if (phone) {
|
if (phone) {
|
||||||
options.phone = phone
|
options.phone = phone
|
||||||
log.debug 'sending SMS'
|
log.debug 'sending SMS'
|
||||||
sendNotification(msg, options)
|
//sendSms(phone, msg)
|
||||||
}
|
}
|
||||||
|
sendNotification(msg, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frequency) {
|
if (frequency) {
|
||||||
state[evt.deviceId] = now()
|
state[evt.deviceId] = now()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user