mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-15 13:10:51 +00:00
Compare commits
130 Commits
PROD_2016.
...
PROD_2016.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd7c6b90d5 | ||
|
|
f5bd580c9e | ||
|
|
d5329dbde3 | ||
|
|
48818cfb06 | ||
|
|
079919260b | ||
|
|
570922e2ac | ||
|
|
53ed9b4d2b | ||
|
|
7149a81c85 | ||
|
|
fb0caa6446 | ||
|
|
3d05d42cb8 | ||
|
|
0f70362e0a | ||
|
|
bc817f8530 | ||
|
|
01b8399893 | ||
|
|
60c2006bfc | ||
|
|
1e4f1223e7 | ||
|
|
6862785d6c | ||
|
|
1858c280a5 | ||
|
|
6b7d0968f6 | ||
|
|
e8101630a3 | ||
|
|
f5ba78b221 | ||
|
|
1e54b93b0c | ||
|
|
bac37f9ca2 | ||
|
|
f0ecb65c09 | ||
|
|
1c0ddd2571 | ||
|
|
b7e0cbda09 | ||
|
|
f80e094bd9 | ||
|
|
ce9ac624d0 | ||
|
|
f3f5cc42c9 | ||
|
|
313fe8b734 | ||
|
|
0d693386d1 | ||
|
|
d1aee1e874 | ||
|
|
3528e7da51 | ||
|
|
5c2e06c98d | ||
|
|
26df619b4f | ||
|
|
af2ea04442 | ||
|
|
383f72580a | ||
|
|
090a306939 | ||
|
|
d0a16c10b2 | ||
|
|
faa65f204d | ||
|
|
bacd335991 | ||
|
|
740e5e096c | ||
|
|
aac2f9b177 | ||
|
|
048eb77e64 | ||
|
|
dadec937fa | ||
|
|
78aa6691c4 | ||
|
|
315918dc6f | ||
|
|
a22f71bc29 | ||
|
|
8eeb29a8a7 | ||
|
|
3d84bca3d7 | ||
|
|
46f47128bd | ||
|
|
9c8398b7a0 | ||
|
|
cdf5d21e8f | ||
|
|
276f7d3b43 | ||
|
|
ce12ad5013 | ||
|
|
beed783d19 | ||
|
|
7f347638d5 | ||
|
|
1f8ce734e7 | ||
|
|
17bf040c7e | ||
|
|
d5b8db99a2 | ||
|
|
6ad4f0990c | ||
|
|
f1c3f5942b | ||
|
|
212c9c4179 | ||
|
|
4898006e4e | ||
|
|
b1c318ef36 | ||
|
|
8eb6001f9f | ||
|
|
d3eb7f756f | ||
|
|
b95ba37364 | ||
|
|
87f8755faf | ||
|
|
555a9f5ab4 | ||
|
|
0744384dbf | ||
|
|
97e0e9d0f8 | ||
|
|
655e756b1b | ||
|
|
7ce7ad86bd | ||
|
|
d90e15ee31 | ||
|
|
cfe25607ac | ||
|
|
f251042954 | ||
|
|
33df9b1ff1 | ||
|
|
24c64608a9 | ||
|
|
dbc2a1e45c | ||
|
|
92cc8afdf7 | ||
|
|
e545842f7c | ||
|
|
c8bda222cb | ||
|
|
17014dd248 | ||
|
|
555edf623a | ||
|
|
feb6ba0e24 | ||
|
|
9d65150bf7 | ||
|
|
99d6e9f668 | ||
|
|
a35f271a8e | ||
|
|
fd4969981f | ||
|
|
1ec110155d | ||
|
|
79b90d741f | ||
|
|
6009bc52ab | ||
|
|
33a8fe108e | ||
|
|
fadc496e1f | ||
|
|
910694f1d1 | ||
|
|
fa9ebed998 | ||
|
|
0a2f2bffc2 | ||
|
|
8dc36eb8f6 | ||
|
|
d0929ab89e | ||
|
|
df6646103a | ||
|
|
014affe1ea | ||
|
|
53fc948b00 | ||
|
|
f389e925d2 | ||
|
|
ef47ec9393 | ||
|
|
62d800e99a | ||
|
|
a79c9c1ade | ||
|
|
4505ca85b2 | ||
|
|
3c0c050b3a | ||
|
|
2cd915ba77 | ||
|
|
4866ecd204 | ||
|
|
f0071aad6d | ||
|
|
0d4a00ae2b | ||
|
|
90384d0852 | ||
|
|
47183ebbff | ||
|
|
d68f70b3dd | ||
|
|
e1b1479cfc | ||
|
|
16e4954f10 | ||
|
|
a1b375c929 | ||
|
|
929f8e1a44 | ||
|
|
ad50582e92 | ||
|
|
6de6704502 | ||
|
|
a5bc475cc1 | ||
|
|
7beb2e3905 | ||
|
|
c7396349f1 | ||
|
|
089cc1a5dd | ||
|
|
7bfa0304af | ||
|
|
1ddd0632c9 | ||
|
|
53d0957383 | ||
|
|
b4a4d83ce7 | ||
|
|
ef29820fa1 |
@@ -274,6 +274,7 @@ 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,7 +254,8 @@ private Map getTemperatureResult(value) {
|
|||||||
return [
|
return [
|
||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText
|
descriptionText: descriptionText,
|
||||||
|
unit: temperatureScale
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ metadata {
|
|||||||
capability "Configuration"
|
capability "Configuration"
|
||||||
capability "Sensor"
|
capability "Sensor"
|
||||||
capability "Battery"
|
capability "Battery"
|
||||||
capability "Health Check"
|
|
||||||
|
|
||||||
attribute "tamper", "enum", ["detected", "clear"]
|
attribute "tamper", "enum", ["detected", "clear"]
|
||||||
attribute "batteryStatus", "string"
|
attribute "batteryStatus", "string"
|
||||||
@@ -328,9 +327,6 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
// allow device user configured or default 16 min to check in; double the periodic reporting interval
|
|
||||||
sendEvent(name: "checkInterval", value: 2* (timeOptionValueMap[reportInterval] ?: (2*8*60)), displayed: false)
|
|
||||||
|
|
||||||
// This sensor joins as a secure device if you double-click the button to include it
|
// This sensor joins as a secure device if you double-click the button to include it
|
||||||
log.debug "${device.displayName} is configuring its settings"
|
log.debug "${device.displayName} is configuring its settings"
|
||||||
def request = []
|
def request = []
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ metadata {
|
|||||||
capability "Configuration"
|
capability "Configuration"
|
||||||
capability "Sensor"
|
capability "Sensor"
|
||||||
capability "Battery"
|
capability "Battery"
|
||||||
capability "Health Check"
|
|
||||||
|
|
||||||
command "configureAfterSecure"
|
command "configureAfterSecure"
|
||||||
|
|
||||||
@@ -248,8 +247,6 @@ def configureAfterSecure() {
|
|||||||
def configure() {
|
def configure() {
|
||||||
// log.debug "configure()"
|
// log.debug "configure()"
|
||||||
//["delay 30000"] + secure(zwave.securityV1.securityCommandsSupportedGet())
|
//["delay 30000"] + secure(zwave.securityV1.securityCommandsSupportedGet())
|
||||||
// allow device 16 min to check in; double the periodic reporting interval
|
|
||||||
sendEvent(name: "checkInterval", value: 2*8*60, displayed: false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private setConfigured() {
|
private setConfigured() {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ metadata {
|
|||||||
capability "Illuminance Measurement"
|
capability "Illuminance Measurement"
|
||||||
capability "Sensor"
|
capability "Sensor"
|
||||||
capability "Battery"
|
capability "Battery"
|
||||||
capability "Health Check"
|
|
||||||
|
|
||||||
fingerprint deviceId: "0x2001", inClusters: "0x30,0x31,0x80,0x84,0x70,0x85,0x72,0x86"
|
fingerprint deviceId: "0x2001", inClusters: "0x30,0x31,0x80,0x84,0x70,0x85,0x72,0x86"
|
||||||
}
|
}
|
||||||
@@ -181,9 +180,6 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
// allow device 10 min to check in; double the periodic reporting interval
|
|
||||||
sendEvent(name: "checkInterval", value: 2*5*60, displayed: false)
|
|
||||||
|
|
||||||
delayBetween([
|
delayBetween([
|
||||||
// send binary sensor report instead of basic set for motion
|
// send binary sensor report instead of basic set for motion
|
||||||
zwave.configurationV1.configurationSet(parameterNumber: 5, size: 1, scaledConfigurationValue: 2).format(),
|
zwave.configurationV1.configurationSet(parameterNumber: 5, size: 1, scaledConfigurationValue: 2).format(),
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ metadata {
|
|||||||
|
|
||||||
command "everywhereJoin"
|
command "everywhereJoin"
|
||||||
command "everywhereLeave"
|
command "everywhereLeave"
|
||||||
|
|
||||||
|
command "forceOff"
|
||||||
|
command "forceOn"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,9 +67,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: "switch.off", icon: "st.Electronics.electronics16", backgroundColor: "#79b821", nextState:"turningOff"
|
state "on", label: '${name}', action: "forceOff", icon: "st.Electronics.electronics16", backgroundColor: "#79b821", nextState:"turningOff"
|
||||||
state "turningOff", label:'TURNING OFF', icon:"st.Electronics.electronics16", backgroundColor:"#ffffff"
|
state "turningOff", label:'TURNING OFF', icon:"st.Electronics.electronics16", backgroundColor:"#ffffff"
|
||||||
state "off", label: '${name}', action: "switch.on", icon: "st.Electronics.electronics16", backgroundColor: "#ffffff", nextState:"turningOn"
|
state "off", label: '${name}', action: "forceOn", icon: "st.Electronics.electronics16", backgroundColor: "#ffffff", nextState:"turningOn"
|
||||||
state "turningOn", label:'TURNING ON', icon:"st.Electronics.electronics16", backgroundColor:"#79b821"
|
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) {
|
||||||
@@ -140,8 +143,22 @@ metadata {
|
|||||||
* one place.
|
* one place.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
def off() { onAction("off") }
|
def off() {
|
||||||
def on() { onAction("on") }
|
if (device.currentState("switch")?.value == "on") {
|
||||||
|
onAction("off")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def forceOff() {
|
||||||
|
onAction("off")
|
||||||
|
}
|
||||||
|
def on() {
|
||||||
|
if (device.currentState("switch")?.value == "off") {
|
||||||
|
onAction("on")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def forceOn() {
|
||||||
|
onAction("on")
|
||||||
|
}
|
||||||
def volup() { onAction("volup") }
|
def volup() { onAction("volup") }
|
||||||
def voldown() { onAction("voldown") }
|
def voldown() { onAction("voldown") }
|
||||||
def preset1() { onAction("1") }
|
def preset1() { onAction("1") }
|
||||||
@@ -240,11 +257,11 @@ def onAction(String user, data=null) {
|
|||||||
def actions = null
|
def actions = null
|
||||||
switch (user) {
|
switch (user) {
|
||||||
case "on":
|
case "on":
|
||||||
actions = boseSetPowerState(true)
|
boseSetPowerState(true)
|
||||||
break
|
break
|
||||||
case "off":
|
case "off":
|
||||||
boseSetNowPlaying(null, "STANDBY")
|
boseSetNowPlaying(null, "STANDBY")
|
||||||
actions = boseSetPowerState(false)
|
boseSetPowerState(false)
|
||||||
break
|
break
|
||||||
case "volume":
|
case "volume":
|
||||||
actions = boseSetVolume(data)
|
actions = boseSetVolume(data)
|
||||||
|
|||||||
@@ -89,14 +89,17 @@ 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"
|
||||||
@@ -169,7 +172,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)
|
sendEvent("name": "heatingSetpoint", "value": degreesInteger, "unit": temperatureScale)
|
||||||
|
|
||||||
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) + "}"
|
||||||
@@ -180,7 +183,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)
|
sendEvent("name": "coolingSetpoint", "value": degreesInteger, "unit": temperatureScale)
|
||||||
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) + "}"
|
||||||
}
|
}
|
||||||
|
|||||||
2
devicetypes/smartthings/cree-bulb.src/.st-ignore
Normal file
2
devicetypes/smartthings/cree-bulb.src/.st-ignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.st-ignore
|
||||||
|
README.md
|
||||||
36
devicetypes/smartthings/cree-bulb.src/README.md
Normal file
36
devicetypes/smartthings/cree-bulb.src/README.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# 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)
|
||||||
@@ -23,6 +23,7 @@ metadata {
|
|||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
capability "Switch"
|
capability "Switch"
|
||||||
capability "Switch Level"
|
capability "Switch Level"
|
||||||
|
capability "Health Check"
|
||||||
|
|
||||||
fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0000,0019"
|
fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0000,0019"
|
||||||
}
|
}
|
||||||
@@ -85,6 +86,13 @@ def setLevel(value) {
|
|||||||
zigbee.setLevel(value) + ["delay 500"] + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report
|
zigbee.setLevel(value) + ["delay 500"] + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
|
* */
|
||||||
|
def ping() {
|
||||||
|
return zigbee.levelRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig()
|
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig()
|
||||||
}
|
}
|
||||||
@@ -95,5 +103,8 @@ def poll() {
|
|||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
log.debug "Configuring Reporting and Bindings."
|
log.debug "Configuring Reporting and Bindings."
|
||||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
|
// Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
|
||||||
|
sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
|
||||||
|
// minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||||
|
zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, isStateChange: isChange, displayed: isDisplayed]
|
event << [value: sendValue, unit: temperatureScale, 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, displayed: false]
|
event << [value: sendValue, unit: temperatureScale, 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(this, heatingValue, coolingValue, deviceId, sendHoldType)) {
|
if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) {
|
||||||
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
|
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale)
|
||||||
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
|
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint, "unit":location.temperatureScale)
|
||||||
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(this, heatingValue, coolingValue, deviceId, sendHoldType)) {
|
if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) {
|
||||||
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
|
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale)
|
||||||
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
|
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint, "unit":location.temperatureScale)
|
||||||
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(this, deviceId)) {
|
if (parent.resumeProgram(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(this, deviceId)"
|
log.error "Error resumeProgram() check parent.resumeProgram(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 (this,"off", deviceId))
|
if (parent.setMode ("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 (this,"heat", deviceId))
|
if (parent.setMode ("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 (this,"auxHeatOnly", deviceId))
|
if (parent.setMode ("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 (this,"cool", deviceId))
|
if (parent.setMode ("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 (this,"auto", deviceId))
|
if (parent.setMode ("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(this, heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) {
|
if (parent.setFanMode(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(this, heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) {
|
if (parent.setFanMode(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 )
|
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale)
|
||||||
|
|
||||||
}
|
}
|
||||||
else if (mode == "cool") {
|
else if (mode == "cool") {
|
||||||
|
|
||||||
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint)
|
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint, "unit":location.temperatureScale)
|
||||||
|
|
||||||
} 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)
|
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,7 +608,7 @@ void raiseSetpoint() {
|
|||||||
targetvalue = maxCoolingSetpoint
|
targetvalue = maxCoolingSetpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false)
|
sendEvent("name":"thermostatSetpoint", "value":targetvalue, "unit":location.temperatureScale, displayed: false)
|
||||||
log.info "In mode $mode raiseSetpoint() to $targetvalue"
|
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, displayed: false)
|
sendEvent("name":"thermostatSetpoint", "value":targetvalue, "unit":location.temperatureScale, 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(this, heatingValue, coolingValue, deviceId, sendHoldType)) {
|
if (parent.setHold(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)
|
sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint, "unit": location.temperatureScale)
|
||||||
sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint)
|
sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint, "unit": location.temperatureScale)
|
||||||
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)
|
sendEvent("name":"heatingSetpoint", "value":degreesInteger, "unit":temperatureScale)
|
||||||
|
|
||||||
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)
|
sendEvent("name":"coolingSetpoint", "value":degreesInteger, "unit":temperatureScale)
|
||||||
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) + "}"
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ metadata {
|
|||||||
capability "Switch"
|
capability "Switch"
|
||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
capability "Sensor"
|
capability "Sensor"
|
||||||
|
capability "Health Check"
|
||||||
|
|
||||||
command "setAdjustedColor"
|
command "setAdjustedColor"
|
||||||
command "reset"
|
command "reset"
|
||||||
@@ -43,7 +44,7 @@ metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
standardTile("reset", "device.reset", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
standardTile("reset", "device.reset", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single"
|
state "default", label:"Reset To White", action:"reset", icon:"st.lights.philips.hue-single"
|
||||||
}
|
}
|
||||||
|
|
||||||
standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
@@ -51,10 +52,14 @@ metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main(["rich-control"])
|
main(["rich-control"])
|
||||||
details(["rich-control", "colorTempSliderControl", "colorTemp", "reset", "refresh"])
|
details(["rich-control", "reset", "refresh"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void installed() {
|
||||||
|
sendEvent(name: "checkInterval", value: 60 * 15, data: [protocol: "lan"], displayed: false)
|
||||||
|
}
|
||||||
|
|
||||||
// parse events into attributes
|
// parse events into attributes
|
||||||
def parse(description) {
|
def parse(description) {
|
||||||
log.debug "parse() - $description"
|
log.debug "parse() - $description"
|
||||||
@@ -75,118 +80,78 @@ def parse(description) {
|
|||||||
// handle commands
|
// handle commands
|
||||||
void on() {
|
void on() {
|
||||||
log.trace parent.on(this)
|
log.trace parent.on(this)
|
||||||
sendEvent(name: "switch", value: "on")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void off() {
|
void off() {
|
||||||
log.trace parent.off(this)
|
log.trace parent.off(this)
|
||||||
sendEvent(name: "switch", value: "off")
|
|
||||||
}
|
|
||||||
|
|
||||||
void nextLevel() {
|
|
||||||
def level = device.latestValue("level") as Integer ?: 0
|
|
||||||
if (level <= 100) {
|
|
||||||
level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
level = 25
|
|
||||||
}
|
|
||||||
setLevel(level)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setLevel(percent) {
|
void setLevel(percent) {
|
||||||
log.debug "Executing 'setLevel'"
|
log.debug "Executing 'setLevel'"
|
||||||
if (verifyPercent(percent)) {
|
if (verifyPercent(percent)) {
|
||||||
parent.setLevel(this, percent)
|
log.trace parent.setLevel(this, percent)
|
||||||
sendEvent(name: "level", value: percent, descriptionText: "Level has changed to ${percent}%")
|
|
||||||
sendEvent(name: "switch", value: "on")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setSaturation(percent) {
|
void setSaturation(percent) {
|
||||||
log.debug "Executing 'setSaturation'"
|
log.debug "Executing 'setSaturation'"
|
||||||
if (verifyPercent(percent)) {
|
if (verifyPercent(percent)) {
|
||||||
parent.setSaturation(this, percent)
|
log.trace parent.setSaturation(this, percent)
|
||||||
sendEvent(name: "saturation", value: percent, displayed: false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setHue(percent) {
|
void setHue(percent) {
|
||||||
log.debug "Executing 'setHue'"
|
log.debug "Executing 'setHue'"
|
||||||
if (verifyPercent(percent)) {
|
if (verifyPercent(percent)) {
|
||||||
parent.setHue(this, percent)
|
log.trace parent.setHue(this, percent)
|
||||||
sendEvent(name: "hue", value: percent, displayed: false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setColor(value) {
|
void setColor(value) {
|
||||||
log.debug "setColor: ${value}, $this"
|
|
||||||
def events = []
|
def events = []
|
||||||
def validValues = [:]
|
def validValues = [:]
|
||||||
|
|
||||||
if (verifyPercent(value.hue)) {
|
if (verifyPercent(value.hue)) {
|
||||||
events << createEvent(name: "hue", value: value.hue, displayed: false)
|
|
||||||
validValues.hue = value.hue
|
validValues.hue = value.hue
|
||||||
}
|
}
|
||||||
if (verifyPercent(value.saturation)) {
|
if (verifyPercent(value.saturation)) {
|
||||||
events << createEvent(name: "saturation", value: value.saturation, displayed: false)
|
|
||||||
validValues.saturation = value.saturation
|
validValues.saturation = value.saturation
|
||||||
}
|
}
|
||||||
if (value.hex != null) {
|
if (value.hex != null) {
|
||||||
if (value.hex ==~ /^\#([A-Fa-f0-9]){6}$/) {
|
if (value.hex ==~ /^\#([A-Fa-f0-9]){6}$/) {
|
||||||
events << createEvent(name: "color", value: value.hex)
|
|
||||||
validValues.hex = value.hex
|
validValues.hex = value.hex
|
||||||
} else {
|
} else {
|
||||||
log.warn "$value.hex is not a valid color"
|
log.warn "$value.hex is not a valid color"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (verifyPercent(value.level)) {
|
if (verifyPercent(value.level)) {
|
||||||
events << createEvent(name: "level", value: value.level, descriptionText: "Level has changed to ${value.level}%")
|
|
||||||
validValues.level = value.level
|
validValues.level = value.level
|
||||||
}
|
}
|
||||||
if (value.switch == "off" || (value.level != null && value.level <= 0)) {
|
if (value.switch == "off" || (value.level != null && value.level <= 0)) {
|
||||||
events << createEvent(name: "switch", value: "off")
|
|
||||||
validValues.switch = "off"
|
validValues.switch = "off"
|
||||||
} else {
|
} else {
|
||||||
events << createEvent(name: "switch", value: "on")
|
|
||||||
validValues.switch = "on"
|
validValues.switch = "on"
|
||||||
}
|
}
|
||||||
if (!events.isEmpty()) {
|
if (!validValues.isEmpty()) {
|
||||||
parent.setColor(this, validValues)
|
log.trace parent.setColor(this, validValues)
|
||||||
}
|
|
||||||
events.each {
|
|
||||||
sendEvent(it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
log.debug "Executing 'reset'"
|
log.debug "Executing 'reset'"
|
||||||
def value = [level:100, saturation:56, hue:23]
|
def value = [hue:20, saturation:2]
|
||||||
setAdjustedColor(value)
|
setAdjustedColor(value)
|
||||||
parent.poll()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setAdjustedColor(value) {
|
void setAdjustedColor(value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
log.trace "setAdjustedColor: ${value}"
|
log.trace "setAdjustedColor: ${value}"
|
||||||
def adjusted = value + [:]
|
def adjusted = value + [:]
|
||||||
adjusted.hue = adjustOutgoingHue(value.hue)
|
|
||||||
// Needed because color picker always sends 100
|
// Needed because color picker always sends 100
|
||||||
adjusted.level = null
|
adjusted.level = null
|
||||||
setColor(adjusted)
|
setColor(adjusted)
|
||||||
} else {
|
} else {
|
||||||
log.warn "Invalid color input"
|
log.warn "Invalid color input $value"
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setColorTemperature(value) {
|
|
||||||
if (value) {
|
|
||||||
log.trace "setColorTemperature: ${value}k"
|
|
||||||
parent.setColorTemperature(this, value)
|
|
||||||
sendEvent(name: "colorTemperature", value: value)
|
|
||||||
sendEvent(name: "switch", value: "on")
|
|
||||||
} else {
|
|
||||||
log.warn "Invalid color temperature"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,22 +160,6 @@ void refresh() {
|
|||||||
parent.manualRefresh()
|
parent.manualRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
def adjustOutgoingHue(percent) {
|
|
||||||
def adjusted = percent
|
|
||||||
if (percent > 31) {
|
|
||||||
if (percent < 63.0) {
|
|
||||||
adjusted = percent + (7 * (percent -30 ) / 32)
|
|
||||||
}
|
|
||||||
else if (percent < 73.0) {
|
|
||||||
adjusted = 69 + (5 * (percent - 62) / 10)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
adjusted = percent + (2 * (100 - percent) / 28)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.info "percent: $percent, adjusted: $adjusted"
|
|
||||||
adjusted
|
|
||||||
}
|
|
||||||
|
|
||||||
def verifyPercent(percent) {
|
def verifyPercent(percent) {
|
||||||
if (percent == null)
|
if (percent == null)
|
||||||
@@ -222,3 +171,7 @@ def verifyPercent(percent) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def ping() {
|
||||||
|
log.debug "${parent.ping(this)}"
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,8 +7,13 @@
|
|||||||
metadata {
|
metadata {
|
||||||
// Automatically generated. Make future change here.
|
// Automatically generated. Make future change here.
|
||||||
definition (name: "Hue Bridge", namespace: "smartthings", author: "SmartThings") {
|
definition (name: "Hue Bridge", namespace: "smartthings", author: "SmartThings") {
|
||||||
attribute "serialNumber", "string"
|
|
||||||
attribute "networkAddress", "string"
|
attribute "networkAddress", "string"
|
||||||
|
// Used to indicate if bridge is reachable or not, i.e. is the bridge connected to the network
|
||||||
|
// Possible values "Online" or "Offline"
|
||||||
|
attribute "status", "string"
|
||||||
|
// Id is the number on the back of the hub, Hue uses last six digits of Mac address
|
||||||
|
// This is also used in the Hue application as ID
|
||||||
|
attribute "idNumber", "string"
|
||||||
}
|
}
|
||||||
|
|
||||||
simulator {
|
simulator {
|
||||||
@@ -17,22 +22,23 @@ metadata {
|
|||||||
|
|
||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
multiAttributeTile(name:"rich-control"){
|
multiAttributeTile(name:"rich-control"){
|
||||||
tileAttribute ("", key: "PRIMARY_CONTROL") {
|
tileAttribute ("device.status", key: "PRIMARY_CONTROL") {
|
||||||
attributeState "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#F3C200"
|
attributeState "Offline", label: '${currentValue}', action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#ffffff"
|
||||||
|
attributeState "Online", label: '${currentValue}', action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#79b821"
|
||||||
}
|
}
|
||||||
tileAttribute ("serialNumber", key: "SECONDARY_CONTROL") {
|
|
||||||
attributeState "default", label:'SN: ${currentValue}'
|
|
||||||
}
|
}
|
||||||
}
|
valueTile("doNotRemove", "v", decoration: "flat", height: 2, width: 6, inactiveLabel: false) {
|
||||||
valueTile("serialNumber", "device.serialNumber", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
|
state "default", label:'If removed, Hue lights will not work properly'
|
||||||
state "default", label:'SN: ${currentValue}'
|
|
||||||
}
|
}
|
||||||
valueTile("networkAddress", "device.networkAddress", decoration: "flat", height: 2, width: 4, inactiveLabel: false) {
|
valueTile("idNumber", "device.idNumber", decoration: "flat", height: 2, width: 6, inactiveLabel: false) {
|
||||||
state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false
|
state "default", label:'ID: ${currentValue}'
|
||||||
|
}
|
||||||
|
valueTile("networkAddress", "device.networkAddress", decoration: "flat", height: 2, width: 6, inactiveLabel: false) {
|
||||||
|
state "default", label:'IP: ${currentValue}'
|
||||||
}
|
}
|
||||||
|
|
||||||
main (["rich-control"])
|
main (["rich-control"])
|
||||||
details(["rich-control", "networkAddress"])
|
details(["rich-control", "doNotRemove", "idNumber", "networkAddress"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +62,7 @@ def parse(description) {
|
|||||||
log.trace "HUE BRIDGE, GENERATING EVENT: $map.name: $map.value"
|
log.trace "HUE BRIDGE, GENERATING EVENT: $map.name: $map.value"
|
||||||
results << createEvent(name: "${map.name}", value: "${map.value}")
|
results << createEvent(name: "${map.name}", value: "${map.value}")
|
||||||
} else {
|
} else {
|
||||||
log.trace "Parsing description"
|
log.trace "Parsing description"
|
||||||
def msg = parseLanMessage(description)
|
def msg = parseLanMessage(description)
|
||||||
if (msg.body) {
|
if (msg.body) {
|
||||||
def contentType = msg.headers["Content-Type"]
|
def contentType = msg.headers["Content-Type"]
|
||||||
@@ -66,13 +72,13 @@ def parse(description) {
|
|||||||
log.info "Bridge response: $msg.body"
|
log.info "Bridge response: $msg.body"
|
||||||
} else {
|
} else {
|
||||||
// Sending Bulbs List to parent"
|
// Sending Bulbs List to parent"
|
||||||
if (parent.state.inBulbDiscovery)
|
if (parent.isInBulbDiscovery())
|
||||||
log.info parent.bulbListHandler(device.hub.id, msg.body)
|
log.info parent.bulbListHandler(device.hub.id, msg.body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (contentType?.contains("xml")) {
|
else if (contentType?.contains("xml")) {
|
||||||
log.debug "HUE BRIDGE ALREADY PRESENT"
|
log.debug "HUE BRIDGE ALREADY PRESENT"
|
||||||
parent.hubVerification(device.hub.id, msg.body)
|
parent.hubVerification(device.hub.id, msg.body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ metadata {
|
|||||||
capability "Switch"
|
capability "Switch"
|
||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
capability "Sensor"
|
capability "Sensor"
|
||||||
|
capability "Health Check"
|
||||||
|
|
||||||
command "setAdjustedColor"
|
command "setAdjustedColor"
|
||||||
command "reset"
|
command "reset"
|
||||||
@@ -43,16 +44,16 @@ metadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2000..6500)") {
|
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2000..6500)") {
|
||||||
state "colorTemperature", action:"color temperature.setColorTemperature"
|
state "colorTemperature", action:"color temperature.setColorTemperature"
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||||
state "colorTemperature", label: '${currentValue} K'
|
state "colorTemperature", label: 'WHITES'
|
||||||
}
|
}
|
||||||
|
|
||||||
standardTile("reset", "device.reset", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
standardTile("reset", "device.reset", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single"
|
state "default", label:"Reset To White", action:"reset", icon:"st.lights.philips.hue-single"
|
||||||
}
|
}
|
||||||
|
|
||||||
standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
@@ -64,6 +65,10 @@ metadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void installed() {
|
||||||
|
sendEvent(name: "checkInterval", value: 60 * 15, data: [protocol: "lan"], displayed: false)
|
||||||
|
}
|
||||||
|
|
||||||
// parse events into attributes
|
// parse events into attributes
|
||||||
def parse(description) {
|
def parse(description) {
|
||||||
log.debug "parse() - $description"
|
log.debug "parse() - $description"
|
||||||
@@ -84,118 +89,86 @@ def parse(description) {
|
|||||||
// handle commands
|
// handle commands
|
||||||
void on() {
|
void on() {
|
||||||
log.trace parent.on(this)
|
log.trace parent.on(this)
|
||||||
sendEvent(name: "switch", value: "on")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void off() {
|
void off() {
|
||||||
log.trace parent.off(this)
|
log.trace parent.off(this)
|
||||||
sendEvent(name: "switch", value: "off")
|
|
||||||
}
|
|
||||||
|
|
||||||
void nextLevel() {
|
|
||||||
def level = device.latestValue("level") as Integer ?: 0
|
|
||||||
if (level <= 100) {
|
|
||||||
level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
level = 25
|
|
||||||
}
|
|
||||||
setLevel(level)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setLevel(percent) {
|
void setLevel(percent) {
|
||||||
log.debug "Executing 'setLevel'"
|
log.debug "Executing 'setLevel'"
|
||||||
if (verifyPercent(percent)) {
|
if (verifyPercent(percent)) {
|
||||||
parent.setLevel(this, percent)
|
log.trace parent.setLevel(this, percent)
|
||||||
sendEvent(name: "level", value: percent, descriptionText: "Level has changed to ${percent}%")
|
|
||||||
sendEvent(name: "switch", value: "on")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setSaturation(percent) {
|
void setSaturation(percent) {
|
||||||
log.debug "Executing 'setSaturation'"
|
log.debug "Executing 'setSaturation'"
|
||||||
if (verifyPercent(percent)) {
|
if (verifyPercent(percent)) {
|
||||||
parent.setSaturation(this, percent)
|
log.trace parent.setSaturation(this, percent)
|
||||||
sendEvent(name: "saturation", value: percent, displayed: false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setHue(percent) {
|
void setHue(percent) {
|
||||||
log.debug "Executing 'setHue'"
|
log.debug "Executing 'setHue'"
|
||||||
if (verifyPercent(percent)) {
|
if (verifyPercent(percent)) {
|
||||||
parent.setHue(this, percent)
|
log.trace parent.setHue(this, percent)
|
||||||
sendEvent(name: "hue", value: percent, displayed: false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setColor(value) {
|
void setColor(value) {
|
||||||
log.debug "setColor: ${value}, $this"
|
|
||||||
def events = []
|
def events = []
|
||||||
def validValues = [:]
|
def validValues = [:]
|
||||||
|
|
||||||
if (verifyPercent(value.hue)) {
|
if (verifyPercent(value.hue)) {
|
||||||
events << createEvent(name: "hue", value: value.hue, displayed: false)
|
|
||||||
validValues.hue = value.hue
|
validValues.hue = value.hue
|
||||||
}
|
}
|
||||||
if (verifyPercent(value.saturation)) {
|
if (verifyPercent(value.saturation)) {
|
||||||
events << createEvent(name: "saturation", value: value.saturation, displayed: false)
|
|
||||||
validValues.saturation = value.saturation
|
validValues.saturation = value.saturation
|
||||||
}
|
}
|
||||||
if (value.hex != null) {
|
if (value.hex != null) {
|
||||||
if (value.hex ==~ /^\#([A-Fa-f0-9]){6}$/) {
|
if (value.hex ==~ /^\#([A-Fa-f0-9]){6}$/) {
|
||||||
events << createEvent(name: "color", value: value.hex)
|
|
||||||
validValues.hex = value.hex
|
validValues.hex = value.hex
|
||||||
} else {
|
} else {
|
||||||
log.warn "$value.hex is not a valid color"
|
log.warn "$value.hex is not a valid color"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (verifyPercent(value.level)) {
|
if (verifyPercent(value.level)) {
|
||||||
events << createEvent(name: "level", value: value.level, descriptionText: "Level has changed to ${value.level}%")
|
|
||||||
validValues.level = value.level
|
validValues.level = value.level
|
||||||
}
|
}
|
||||||
if (value.switch == "off" || (value.level != null && value.level <= 0)) {
|
if (value.switch == "off" || (value.level != null && value.level <= 0)) {
|
||||||
events << createEvent(name: "switch", value: "off")
|
|
||||||
validValues.switch = "off"
|
validValues.switch = "off"
|
||||||
} else {
|
} else {
|
||||||
events << createEvent(name: "switch", value: "on")
|
|
||||||
validValues.switch = "on"
|
validValues.switch = "on"
|
||||||
}
|
}
|
||||||
if (!events.isEmpty()) {
|
if (!validValues.isEmpty()) {
|
||||||
parent.setColor(this, validValues)
|
log.trace parent.setColor(this, validValues)
|
||||||
}
|
|
||||||
events.each {
|
|
||||||
sendEvent(it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
log.debug "Executing 'reset'"
|
log.debug "Executing 'reset'"
|
||||||
def value = [level:100, saturation:56, hue:23]
|
setColorTemperature(4000)
|
||||||
setAdjustedColor(value)
|
|
||||||
parent.poll()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setAdjustedColor(value) {
|
void setAdjustedColor(value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
log.trace "setAdjustedColor: ${value}"
|
log.trace "setAdjustedColor: ${value}"
|
||||||
def adjusted = value + [:]
|
def adjusted = value + [:]
|
||||||
adjusted.hue = adjustOutgoingHue(value.hue)
|
|
||||||
// Needed because color picker always sends 100
|
// Needed because color picker always sends 100
|
||||||
adjusted.level = null
|
adjusted.level = null
|
||||||
setColor(adjusted)
|
setColor(adjusted)
|
||||||
} else {
|
} else {
|
||||||
log.warn "Invalid color input"
|
log.warn "Invalid color input $value"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setColorTemperature(value) {
|
void setColorTemperature(value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
log.trace "setColorTemperature: ${value}k"
|
log.trace "setColorTemperature: ${value}k"
|
||||||
parent.setColorTemperature(this, value)
|
log.trace parent.setColorTemperature(this, value)
|
||||||
sendEvent(name: "colorTemperature", value: value)
|
|
||||||
sendEvent(name: "switch", value: "on")
|
|
||||||
} else {
|
} else {
|
||||||
log.warn "Invalid color temperature"
|
log.warn "Invalid color temperature $value"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,23 +177,6 @@ void refresh() {
|
|||||||
parent.manualRefresh()
|
parent.manualRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
def adjustOutgoingHue(percent) {
|
|
||||||
def adjusted = percent
|
|
||||||
if (percent > 31) {
|
|
||||||
if (percent < 63.0) {
|
|
||||||
adjusted = percent + (7 * (percent -30 ) / 32)
|
|
||||||
}
|
|
||||||
else if (percent < 73.0) {
|
|
||||||
adjusted = 69 + (5 * (percent - 62) / 10)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
adjusted = percent + (2 * (100 - percent) / 28)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.info "percent: $percent, adjusted: $adjusted"
|
|
||||||
adjusted
|
|
||||||
}
|
|
||||||
|
|
||||||
def verifyPercent(percent) {
|
def verifyPercent(percent) {
|
||||||
if (percent == null)
|
if (percent == null)
|
||||||
return false
|
return false
|
||||||
@@ -231,3 +187,7 @@ def verifyPercent(percent) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def ping() {
|
||||||
|
log.trace "${parent.ping(this)}"
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ metadata {
|
|||||||
capability "Switch"
|
capability "Switch"
|
||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
capability "Sensor"
|
capability "Sensor"
|
||||||
|
capability "Health Check"
|
||||||
|
|
||||||
command "refresh"
|
command "refresh"
|
||||||
}
|
}
|
||||||
@@ -48,6 +49,10 @@ metadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void installed() {
|
||||||
|
sendEvent(name: "checkInterval", value: 60 * 15, data: [protocol: "lan"], displayed: false)
|
||||||
|
}
|
||||||
|
|
||||||
// parse events into attributes
|
// parse events into attributes
|
||||||
def parse(description) {
|
def parse(description) {
|
||||||
log.debug "parse() - $description"
|
log.debug "parse() - $description"
|
||||||
@@ -68,20 +73,16 @@ def parse(description) {
|
|||||||
// handle commands
|
// handle commands
|
||||||
void on() {
|
void on() {
|
||||||
log.trace parent.on(this)
|
log.trace parent.on(this)
|
||||||
sendEvent(name: "switch", value: "on")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void off() {
|
void off() {
|
||||||
log.trace parent.off(this)
|
log.trace parent.off(this)
|
||||||
sendEvent(name: "switch", value: "off")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setLevel(percent) {
|
void setLevel(percent) {
|
||||||
log.debug "Executing 'setLevel'"
|
log.debug "Executing 'setLevel'"
|
||||||
if (percent != null && percent >= 0 && percent <= 100) {
|
if (percent != null && percent >= 0 && percent <= 100) {
|
||||||
parent.setLevel(this, percent)
|
parent.setLevel(this, percent)
|
||||||
sendEvent(name: "level", value: percent)
|
|
||||||
sendEvent(name: "switch", value: "on")
|
|
||||||
} else {
|
} else {
|
||||||
log.warn "$percent is not 0-100"
|
log.warn "$percent is not 0-100"
|
||||||
}
|
}
|
||||||
@@ -91,3 +92,7 @@ void refresh() {
|
|||||||
log.debug "Executing 'refresh'"
|
log.debug "Executing 'refresh'"
|
||||||
parent.manualRefresh()
|
parent.manualRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def ping() {
|
||||||
|
log.debug "${parent.ping(this)}"
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ metadata {
|
|||||||
capability "Color Temperature"
|
capability "Color Temperature"
|
||||||
capability "Switch"
|
capability "Switch"
|
||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
|
capability "Health Check"
|
||||||
|
|
||||||
command "refresh"
|
command "refresh"
|
||||||
}
|
}
|
||||||
@@ -36,12 +37,12 @@ metadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2000..6500)") {
|
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2200..6500)") {
|
||||||
state "colorTemperature", action:"color temperature.setColorTemperature"
|
state "colorTemperature", action:"color temperature.setColorTemperature"
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||||
state "colorTemperature", label: '${currentValue} K'
|
state "colorTemperature", label: 'WHITES'
|
||||||
}
|
}
|
||||||
|
|
||||||
standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
@@ -53,6 +54,10 @@ metadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void installed() {
|
||||||
|
sendEvent(name: "checkInterval", value: 60 * 15, data: [protocol: "lan"], displayed: false)
|
||||||
|
}
|
||||||
|
|
||||||
// parse events into attributes
|
// parse events into attributes
|
||||||
def parse(description) {
|
def parse(description) {
|
||||||
log.debug "parse() - $description"
|
log.debug "parse() - $description"
|
||||||
@@ -73,20 +78,16 @@ def parse(description) {
|
|||||||
// handle commands
|
// handle commands
|
||||||
void on() {
|
void on() {
|
||||||
log.trace parent.on(this)
|
log.trace parent.on(this)
|
||||||
sendEvent(name: "switch", value: "on")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void off() {
|
void off() {
|
||||||
log.trace parent.off(this)
|
log.trace parent.off(this)
|
||||||
sendEvent(name: "switch", value: "off")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setLevel(percent) {
|
void setLevel(percent) {
|
||||||
log.debug "Executing 'setLevel'"
|
log.debug "Executing 'setLevel'"
|
||||||
if (percent != null && percent >= 0 && percent <= 100) {
|
if (percent != null && percent >= 0 && percent <= 100) {
|
||||||
parent.setLevel(this, percent)
|
log.trace parent.setLevel(this, percent)
|
||||||
sendEvent(name: "level", value: percent)
|
|
||||||
sendEvent(name: "switch", value: "on")
|
|
||||||
} else {
|
} else {
|
||||||
log.warn "$percent is not 0-100"
|
log.warn "$percent is not 0-100"
|
||||||
}
|
}
|
||||||
@@ -95,9 +96,7 @@ void setLevel(percent) {
|
|||||||
void setColorTemperature(value) {
|
void setColorTemperature(value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
log.trace "setColorTemperature: ${value}k"
|
log.trace "setColorTemperature: ${value}k"
|
||||||
parent.setColorTemperature(this, value)
|
log.trace parent.setColorTemperature(this, value)
|
||||||
sendEvent(name: "colorTemperature", value: value)
|
|
||||||
sendEvent(name: "switch", value: "on")
|
|
||||||
} else {
|
} else {
|
||||||
log.warn "Invalid color temperature"
|
log.warn "Invalid color temperature"
|
||||||
}
|
}
|
||||||
@@ -108,3 +107,6 @@ void refresh() {
|
|||||||
parent.manualRefresh()
|
parent.manualRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def ping() {
|
||||||
|
log.debug "${parent.ping(this)}"
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
* for the specific language governing permissions and limitations under the License.
|
* for the specific language governing permissions and limitations under the License.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
|
||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "NYCE Motion Sensor", namespace: "smartthings", author: "SmartThings") {
|
definition (name: "NYCE Motion Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||||
@@ -143,51 +144,14 @@ private Map parseReportAttributeMessage(String description) {
|
|||||||
|
|
||||||
|
|
||||||
private Map parseIasMessage(String description) {
|
private Map parseIasMessage(String description) {
|
||||||
List parsedMsg = description.split(' ')
|
ZoneStatus zs = zigbee.parseZoneStatus(description)
|
||||||
String msgCode = parsedMsg[2]
|
Map resultMap = [:]
|
||||||
|
|
||||||
Map resultMap = [:]
|
|
||||||
switch(msgCode) {
|
|
||||||
case '0x0030': // Closed/No Motion/Dry
|
|
||||||
log.debug 'no motion'
|
|
||||||
resultMap.name = 'motion'
|
|
||||||
resultMap.value = 'inactive'
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0032': // Open/Motion/Wet
|
resultMap.name = 'motion'
|
||||||
log.debug 'motion'
|
resultMap.value = zs.isAlarm2Set() ? 'active' : 'inactive'
|
||||||
resultMap.name = 'motion'
|
log.debug(zs.isAlarm2Set() ? 'motion' : 'no motion')
|
||||||
resultMap.value = 'active'
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0032': // Tamper Alarm
|
return resultMap
|
||||||
log.debug 'motion with tamper alarm'
|
|
||||||
resultMap.name = 'motion'
|
|
||||||
resultMap.value = 'active'
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0033': // Battery Alarm
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0034': // Supervision Report
|
|
||||||
log.debug 'no motion with tamper alarm'
|
|
||||||
resultMap.name = 'motion'
|
|
||||||
resultMap.value = 'inactive'
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0035': // Restore Report
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0036': // Trouble/Failure
|
|
||||||
log.debug 'motion with failure alarm'
|
|
||||||
resultMap.name = 'motion'
|
|
||||||
resultMap.value = 'active'
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0038': // Test Mode
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return resultMap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def refresh()
|
def refresh()
|
||||||
|
|||||||
@@ -13,7 +13,10 @@
|
|||||||
* for the specific language governing permissions and limitations under the License.
|
* for the specific language governing permissions and limitations under the License.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
|
||||||
|
|
||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "NYCE Open/Closed Sensor", namespace: "smartthings", author: "NYCE") {
|
definition (name: "NYCE Open/Closed Sensor", namespace: "smartthings", author: "NYCE") {
|
||||||
capability "Battery"
|
capability "Battery"
|
||||||
@@ -219,40 +222,33 @@ private Map parseReportAttributeMessage(String description) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List parseIasMessage(String description) {
|
private List parseIasMessage(String description) {
|
||||||
List parsedMsg = description.split(" ")
|
ZoneStatus zs = zigbee.parseZoneStatus(description)
|
||||||
String msgCode = parsedMsg[2]
|
log.debug "parseIasMessage: $description"
|
||||||
|
|
||||||
List resultListMap = []
|
List resultListMap = []
|
||||||
Map resultMap_battery = [:]
|
Map resultMap_battery = [:]
|
||||||
Map resultMap_battery_state = [:]
|
Map resultMap_battery_state = [:]
|
||||||
Map resultMap_sensor = [:]
|
Map resultMap_sensor = [:]
|
||||||
|
|
||||||
// Relevant bit field definitions from ZigBee spec
|
resultMap_sensor.name = "contact"
|
||||||
def BATTERY_BIT = ( 1 << 3 )
|
resultMap_sensor.value = zs.isAlarm1Set() ? "open" : "closed"
|
||||||
def TROUBLE_BIT = ( 1 << 6 )
|
|
||||||
def SENSOR_BIT = ( 1 << 0 ) // it's ALARM1 bit from the ZCL spec
|
|
||||||
|
|
||||||
// Convert hex string to integer
|
|
||||||
def zoneStatus = Integer.parseInt(msgCode[-4..-1],16)
|
|
||||||
|
|
||||||
log.debug "parseIasMessage: zoneStatus: ${zoneStatus}"
|
|
||||||
|
|
||||||
// Check each relevant bit, create map for it, and add to list
|
// Check each relevant bit, create map for it, and add to list
|
||||||
log.debug "parseIasMessage: Battery Status ${zoneStatus & BATTERY_BIT}"
|
log.debug "parseIasMessage: Battery Status ${zs.battery}"
|
||||||
log.debug "parseIasMessage: Trouble Status ${zoneStatus & TROUBLE_BIT}"
|
log.debug "parseIasMessage: Trouble Status ${zs.trouble}"
|
||||||
log.debug "parseIasMessage: Sensor Status ${zoneStatus & SENSOR_BIT}"
|
log.debug "parseIasMessage: Sensor Status ${zs.alarm1}"
|
||||||
|
|
||||||
/* Comment out this path to check the battery state to avoid overwriting the
|
/* Comment out this path to check the battery state to avoid overwriting the
|
||||||
battery value (Change log #2), but keep these conditions for later use
|
battery value (Change log #2), but keep these conditions for later use
|
||||||
resultMap_battery_state.name = "battery_state"
|
resultMap_battery_state.name = "battery_state"
|
||||||
if (zoneStatus & TROUBLE_BIT) {
|
if (zs.isTroubleSet()) {
|
||||||
resultMap_battery_state.value = "failed"
|
resultMap_battery_state.value = "failed"
|
||||||
|
|
||||||
resultMap_battery.name = "battery"
|
resultMap_battery.name = "battery"
|
||||||
resultMap_battery.value = 0
|
resultMap_battery.value = 0
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (zoneStatus & BATTERY_BIT) {
|
if (zs.isBatterySet()) {
|
||||||
resultMap_battery_state.value = "low"
|
resultMap_battery_state.value = "low"
|
||||||
|
|
||||||
// to generate low battery notification by the platform
|
// to generate low battery notification by the platform
|
||||||
@@ -270,9 +266,6 @@ private List parseIasMessage(String description) {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
resultMap_sensor.name = "contact"
|
|
||||||
resultMap_sensor.value = (zoneStatus & SENSOR_BIT) ? "open" : "closed"
|
|
||||||
|
|
||||||
resultListMap << resultMap_battery_state
|
resultListMap << resultMap_battery_state
|
||||||
resultListMap << resultMap_battery
|
resultListMap << resultMap_battery
|
||||||
resultListMap << resultMap_sensor
|
resultListMap << resultMap_sensor
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ def parse(String description) {
|
|||||||
|
|
||||||
if (descMap.cluster == "0300") {
|
if (descMap.cluster == "0300") {
|
||||||
if(descMap.attrId == "0000"){ //Hue Attribute
|
if(descMap.attrId == "0000"){ //Hue Attribute
|
||||||
def hueValue = Math.round(convertHexToInt(descMap.value) / 255 * 360)
|
def hueValue = Math.round(convertHexToInt(descMap.value) / 255 * 100)
|
||||||
log.debug "Hue value returned is $hueValue"
|
log.debug "Hue value returned is $hueValue"
|
||||||
sendEvent(name: "hue", value: hueValue, displayed:false)
|
sendEvent(name: "hue", value: hueValue, displayed:false)
|
||||||
}
|
}
|
||||||
@@ -203,7 +203,7 @@ def setLevel(value) {
|
|||||||
|
|
||||||
//input Hue Integer values; returns color name for saturation 100%
|
//input Hue Integer values; returns color name for saturation 100%
|
||||||
private getColorName(hueValue){
|
private getColorName(hueValue){
|
||||||
if(hueValue>360 || hueValue<0)
|
if(hueValue>100 || hueValue<0)
|
||||||
return
|
return
|
||||||
|
|
||||||
hueValue = Math.round(hueValue / 100 * 360)
|
hueValue = Math.round(hueValue / 100 * 360)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/*
|
/*
|
||||||
Osram Flex RGBW Light Strip
|
Osram Flex RGBW Light Strip
|
||||||
|
|
||||||
Osram bulbs have a firmware issue causing it to forget its dimming level when turned off (via commands). Handling
|
Osram bulbs have a firmware issue causing it to forget its dimming level when turned off (via commands). Handling
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "OSRAM LIGHTIFY LED Flexible Strip RGBW", namespace: "smartthings", author: "SmartThings") {
|
definition (name: "OSRAM LIGHTIFY LED Flexible Strip RGBW", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
|
||||||
capability "Color Temperature"
|
capability "Color Temperature"
|
||||||
capability "Actuator"
|
capability "Actuator"
|
||||||
capability "Switch"
|
capability "Switch"
|
||||||
@@ -18,7 +18,7 @@ metadata {
|
|||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
capability "Sensor"
|
capability "Sensor"
|
||||||
capability "Color Control"
|
capability "Color Control"
|
||||||
|
|
||||||
attribute "colorName", "string"
|
attribute "colorName", "string"
|
||||||
|
|
||||||
command "setAdjustedColor"
|
command "setAdjustedColor"
|
||||||
@@ -49,7 +49,7 @@ metadata {
|
|||||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
}
|
}
|
||||||
|
|
||||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") {
|
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") {
|
||||||
state "colorTemperature", action:"color temperature.setColorTemperature"
|
state "colorTemperature", action:"color temperature.setColorTemperature"
|
||||||
}
|
}
|
||||||
@@ -118,7 +118,7 @@ def parse(String description) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(descMap.attrId == "0000"){ //Hue Attribute
|
else if(descMap.attrId == "0000"){ //Hue Attribute
|
||||||
def hueValue = Math.round(convertHexToInt(descMap.value) / 255 * 360)
|
def hueValue = Math.round(convertHexToInt(descMap.value) / 255 * 100)
|
||||||
log.debug "Hue value returned is $hueValue"
|
log.debug "Hue value returned is $hueValue"
|
||||||
sendEvent(name: "hue", value: hueValue, displayed:false)
|
sendEvent(name: "hue", value: hueValue, displayed:false)
|
||||||
}
|
}
|
||||||
@@ -274,7 +274,7 @@ private getGenericName(value){
|
|||||||
|
|
||||||
//input Hue Integer values; returns color name for saturation 100%
|
//input Hue Integer values; returns color name for saturation 100%
|
||||||
private getColorName(hueValue){
|
private getColorName(hueValue){
|
||||||
if(hueValue>360 || hueValue<0)
|
if(hueValue>100 || hueValue<0)
|
||||||
return
|
return
|
||||||
|
|
||||||
hueValue = Math.round(hueValue / 100 * 360)
|
hueValue = Math.round(hueValue / 100 * 360)
|
||||||
@@ -449,7 +449,7 @@ def setColor(value){
|
|||||||
def level = hex(value.level * 255 / 100)
|
def level = hex(value.level * 255 / 100)
|
||||||
cmd << zigbeeSetLevel(level)
|
cmd << zigbeeSetLevel(level)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.switch == "off") {
|
if (value.switch == "off") {
|
||||||
cmd << "delay 150"
|
cmd << "delay 150"
|
||||||
cmd << off()
|
cmd << off()
|
||||||
|
|||||||
2
devicetypes/smartthings/smartpower-outlet.src/.st-ignore
Normal file
2
devicetypes/smartthings/smartpower-outlet.src/.st-ignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.st-ignore
|
||||||
|
README.md
|
||||||
38
devicetypes/smartthings/smartpower-outlet.src/README.md
Normal file
38
devicetypes/smartthings/smartpower-outlet.src/README.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# 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)
|
||||||
@@ -116,14 +116,22 @@ def off() {
|
|||||||
def on() {
|
def on() {
|
||||||
zigbee.on()
|
zigbee.on()
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
|
* */
|
||||||
|
def ping() {
|
||||||
|
return zigbee.onOffRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
zigbee.onOffRefresh() + zigbee.refreshData("0x0B04", "0x050B")
|
zigbee.onOffRefresh() + zigbee.electricMeasurementPowerRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
sendEvent(name: "checkInterval", value: 1200, displayed: false)
|
// Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
|
||||||
zigbee.onOffConfig() + powerConfig() + refresh()
|
sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
|
||||||
|
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||||
|
zigbee.onOffConfig(0, 300) + powerConfig() + refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
//power config for devices with min reporting interval as 1 seconds and reporting interval if no activity as 10min (600s)
|
//power config for devices with min reporting interval as 1 seconds and reporting interval if no activity as 10min (600s)
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.st-ignore
|
||||||
|
README.md
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# 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)
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
* License for the specific language governing permissions and limitations
|
* License for the specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
|
||||||
|
|
||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "SmartSense Moisture Sensor",namespace: "smartthings", author: "SmartThings", category: "C2") {
|
definition (name: "SmartSense Moisture Sensor",namespace: "smartthings", author: "SmartThings", category: "C2") {
|
||||||
@@ -22,6 +24,7 @@ metadata {
|
|||||||
capability "Temperature Measurement"
|
capability "Temperature Measurement"
|
||||||
capability "Water Sensor"
|
capability "Water Sensor"
|
||||||
capability "Health Check"
|
capability "Health Check"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
command "enrollResponse"
|
command "enrollResponse"
|
||||||
|
|
||||||
@@ -169,42 +172,9 @@ private Map parseCustomMessage(String description) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Map parseIasMessage(String description) {
|
private Map parseIasMessage(String description) {
|
||||||
List parsedMsg = description.split(' ')
|
ZoneStatus zs = zigbee.parseZoneStatus(description)
|
||||||
String msgCode = parsedMsg[2]
|
|
||||||
|
|
||||||
Map resultMap = [:]
|
return zs.isAlarm1Set() ? getMoistureResult('wet') : getMoistureResult('dry')
|
||||||
switch(msgCode) {
|
|
||||||
case '0x0020': // Closed/No Motion/Dry
|
|
||||||
resultMap = getMoistureResult('dry')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0021': // Open/Motion/Wet
|
|
||||||
resultMap = getMoistureResult('wet')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0022': // Tamper Alarm
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0023': // Battery Alarm
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0024': // Supervision Report
|
|
||||||
log.debug 'dry with tamper alarm'
|
|
||||||
resultMap = getMoistureResult('dry')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0025': // Restore Report
|
|
||||||
log.debug 'water with tamper alarm'
|
|
||||||
resultMap = getMoistureResult('wet')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0026': // Trouble/Failure
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0028': // Test Mode
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return resultMap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def getTemperature(value) {
|
def getTemperature(value) {
|
||||||
@@ -256,6 +226,8 @@ private Map getBatteryResult(rawValue) {
|
|||||||
def maxVolts = 3.0
|
def maxVolts = 3.0
|
||||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||||
def roundedPct = Math.round(pct * 100)
|
def roundedPct = Math.round(pct * 100)
|
||||||
|
if (roundedPct <= 0)
|
||||||
|
roundedPct = 1
|
||||||
result.value = Math.min(100, roundedPct)
|
result.value = Math.min(100, roundedPct)
|
||||||
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
||||||
}
|
}
|
||||||
@@ -282,7 +254,8 @@ private Map getTemperatureResult(value) {
|
|||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText,
|
descriptionText: descriptionText,
|
||||||
translatable: true
|
translatable: true,
|
||||||
|
unit: temperatureScale
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,6 +274,13 @@ private Map getMoistureResult(value) {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
|
* */
|
||||||
|
def ping() {
|
||||||
|
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
||||||
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
log.debug "Refreshing Temperature and Battery"
|
log.debug "Refreshing Temperature and Battery"
|
||||||
def refreshCmds = [
|
def refreshCmds = [
|
||||||
@@ -312,23 +292,19 @@ def refresh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
sendEvent(name: "checkInterval", value: 7200, displayed: false)
|
// Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
|
||||||
|
sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
|
||||||
|
|
||||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||||
def configCmds = [
|
def enrollCmds = [
|
||||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
|
|
||||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
|
||||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
|
|
||||||
]
|
]
|
||||||
return configCmds + refresh() // send refresh cmds as part of config
|
|
||||||
|
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||||
|
// battery minReport 30 seconds, maxReportTime 6 hrs by default
|
||||||
|
return enrollCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) + refresh() // send refresh cmds as part of config
|
||||||
}
|
}
|
||||||
|
|
||||||
def enrollResponse() {
|
def enrollResponse() {
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ Works with:
|
|||||||
## Table of contents
|
## Table of contents
|
||||||
|
|
||||||
* [Capabilities](#capabilities)
|
* [Capabilities](#capabilities)
|
||||||
* [Health]($health)
|
* [Health](#device-health)
|
||||||
|
* [Battery](#battery-specification)
|
||||||
|
|
||||||
## Capabilities
|
## Capabilities
|
||||||
|
|
||||||
@@ -21,10 +22,24 @@ Works with:
|
|||||||
|
|
||||||
## Device Health
|
## Device Health
|
||||||
|
|
||||||
A Category C2 motion sensor that has 120min check-in interval
|
A Category C2 motion sensor with maxReportTime of 1 hr.
|
||||||
|
Check-in interval is double the value of maxReportTime for Zigbee device.
|
||||||
|
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||||
|
Check-in interval = 2*60 = 120 min
|
||||||
|
|
||||||
|
## Battery Specification
|
||||||
|
|
||||||
|
One 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)
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
* License for the specific language governing permissions and limitations
|
* License for the specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
|
||||||
|
|
||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "SmartSense Motion Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
|
definition (name: "SmartSense Motion Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
|
||||||
@@ -22,6 +24,7 @@ metadata {
|
|||||||
capability "Temperature Measurement"
|
capability "Temperature Measurement"
|
||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
capability "Health Check"
|
capability "Health Check"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
command "enrollResponse"
|
command "enrollResponse"
|
||||||
|
|
||||||
@@ -182,44 +185,10 @@ private Map parseCustomMessage(String description) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Map parseIasMessage(String description) {
|
private Map parseIasMessage(String description) {
|
||||||
List parsedMsg = description.split(' ')
|
ZoneStatus zs = zigbee.parseZoneStatus(description)
|
||||||
String msgCode = parsedMsg[2]
|
|
||||||
|
|
||||||
Map resultMap = [:]
|
// Some sensor models that use this DTH use alarm1 and some use alarm2 to signify motion
|
||||||
switch(msgCode) {
|
return (zs.isAlarm1Set() || zs.isAlarm2Set()) ? getMotionResult('active') : getMotionResult('inactive')
|
||||||
case '0x0020': // Closed/No Motion/Dry
|
|
||||||
resultMap = getMotionResult('inactive')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0021': // Open/Motion/Wet
|
|
||||||
resultMap = getMotionResult('active')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0022': // Tamper Alarm
|
|
||||||
log.debug 'motion with tamper alarm'
|
|
||||||
resultMap = getMotionResult('active')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0023': // Battery Alarm
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0024': // Supervision Report
|
|
||||||
log.debug 'no motion with tamper alarm'
|
|
||||||
resultMap = getMotionResult('inactive')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0025': // Restore Report
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0026': // Trouble/Failure
|
|
||||||
log.debug 'motion with failure alarm'
|
|
||||||
resultMap = getMotionResult('active')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0028': // Test Mode
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return resultMap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def getTemperature(value) {
|
def getTemperature(value) {
|
||||||
@@ -272,6 +241,8 @@ private Map getBatteryResult(rawValue) {
|
|||||||
def maxVolts = 3.0
|
def maxVolts = 3.0
|
||||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||||
def roundedPct = Math.round(pct * 100)
|
def roundedPct = Math.round(pct * 100)
|
||||||
|
if (roundedPct <= 0)
|
||||||
|
roundedPct = 1
|
||||||
result.value = Math.min(100, roundedPct)
|
result.value = Math.min(100, roundedPct)
|
||||||
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
||||||
}
|
}
|
||||||
@@ -298,7 +269,8 @@ private Map getTemperatureResult(value) {
|
|||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText,
|
descriptionText: descriptionText,
|
||||||
translatable: true
|
translatable: true,
|
||||||
|
unit: temperatureScale
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,6 +285,13 @@ private Map getMotionResult(value) {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
|
* */
|
||||||
|
def ping() {
|
||||||
|
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
||||||
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
log.debug "refresh called"
|
log.debug "refresh called"
|
||||||
def refreshCmds = [
|
def refreshCmds = [
|
||||||
@@ -324,24 +303,19 @@ def refresh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
sendEvent(name: "checkInterval", value: 7200, displayed: false)
|
// Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
|
||||||
|
sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
|
||||||
|
|
||||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||||
|
|
||||||
def configCmds = [
|
def enrollCmds = [
|
||||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
|
||||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
|
|
||||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
|
||||||
]
|
]
|
||||||
return configCmds + refresh() // send refresh cmds as part of config
|
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||||
|
// battery minReport 30 seconds, maxReportTime 6 hrs by default
|
||||||
|
return enrollCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) + refresh() // send refresh cmds as part of config
|
||||||
}
|
}
|
||||||
|
|
||||||
def enrollResponse() {
|
def enrollResponse() {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
//DEPRECATED - Using the smartsense-motion-sensor.groovy DTH for this device. Users need to be moved before deleting this DTH
|
//DEPRECATED - Using the smartsense-motion-sensor.groovy DTH for this device. Users need to be moved before deleting this DTH
|
||||||
|
import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
|
||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "SmartSense Motion/Temp Sensor", namespace: "smartthings", author: "SmartThings") {
|
definition (name: "SmartSense Motion/Temp Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||||
@@ -168,44 +169,8 @@ private Map parseCustomMessage(String description) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Map parseIasMessage(String description) {
|
private Map parseIasMessage(String description) {
|
||||||
List parsedMsg = description.split(' ')
|
ZoneStatus zs = zigbee.parseZoneStatus(description)
|
||||||
String msgCode = parsedMsg[2]
|
return (zs.isAlarm1Set() || zs.isAlarm2Set()) ? getMotionResult('active') : getMotionResult('inactive')
|
||||||
|
|
||||||
Map resultMap = [:]
|
|
||||||
switch(msgCode) {
|
|
||||||
case '0x0020': // Closed/No Motion/Dry
|
|
||||||
resultMap = getMotionResult('inactive')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0021': // Open/Motion/Wet
|
|
||||||
resultMap = getMotionResult('active')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0022': // Tamper Alarm
|
|
||||||
log.debug 'motion with tamper alarm'
|
|
||||||
resultMap = getMotionResult('active')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0023': // Battery Alarm
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0024': // Supervision Report
|
|
||||||
log.debug 'no motion with tamper alarm'
|
|
||||||
resultMap = getMotionResult('inactive')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0025': // Restore Report
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0026': // Trouble/Failure
|
|
||||||
log.debug 'motion with failure alarm'
|
|
||||||
resultMap = getMotionResult('active')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0028': // Test Mode
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return resultMap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def getTemperature(value) {
|
def getTemperature(value) {
|
||||||
@@ -241,6 +206,8 @@ private Map getBatteryResult(rawValue) {
|
|||||||
def maxVolts = 3.0
|
def maxVolts = 3.0
|
||||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||||
def roundedPct = Math.round(pct * 100)
|
def roundedPct = Math.round(pct * 100)
|
||||||
|
if (roundedPct <= 0)
|
||||||
|
roundedPct = 1
|
||||||
result.value = Math.min(100, roundedPct)
|
result.value = Math.min(100, roundedPct)
|
||||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||||
}
|
}
|
||||||
@@ -261,7 +228,8 @@ private Map getTemperatureResult(value) {
|
|||||||
return [
|
return [
|
||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText
|
descriptionText: descriptionText,
|
||||||
|
unit: temperatureScale
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.st-ignore
|
||||||
|
README.md
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# 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)
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
* License for the specific language governing permissions and limitations
|
* License for the specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
|
||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
|
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
|
||||||
@@ -224,47 +225,13 @@ private Map parseCustomMessage(String description) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Map parseIasMessage(String description) {
|
private Map parseIasMessage(String description) {
|
||||||
List parsedMsg = description.split(' ')
|
ZoneStatus zs = zigbee.parseZoneStatus(description)
|
||||||
String msgCode = parsedMsg[2]
|
|
||||||
|
|
||||||
Map resultMap = [:]
|
Map resultMap = [:]
|
||||||
switch(msgCode) {
|
|
||||||
case '0x0020': // Closed/No Motion/Dry
|
|
||||||
if (garageSensor != "Yes"){
|
if (garageSensor != "Yes"){
|
||||||
resultMap = getContactResult('closed')
|
resultMap = zs.isAlarm1Set() ? getContactResult('open') : getContactResult('closed')
|
||||||
}
|
}
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0021': // Open/Motion/Wet
|
|
||||||
if (garageSensor != "Yes"){
|
|
||||||
resultMap = getContactResult('open')
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0022': // Tamper Alarm
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0023': // Battery Alarm
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0024': // Supervision Report
|
|
||||||
if (garageSensor != "Yes"){
|
|
||||||
resultMap = getContactResult('closed')
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0025': // Restore Report
|
|
||||||
if (garageSensor != "Yes"){
|
|
||||||
resultMap = getContactResult('open')
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0026': // Trouble/Failure
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0028': // Test Mode
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return resultMap
|
return resultMap
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,6 +306,8 @@ private Map getBatteryResult(rawValue) {
|
|||||||
def maxVolts = 3.0
|
def maxVolts = 3.0
|
||||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||||
def roundedPct = Math.round(pct * 100)
|
def roundedPct = Math.round(pct * 100)
|
||||||
|
if (roundedPct <= 0)
|
||||||
|
roundedPct = 1
|
||||||
result.value = Math.min(100, roundedPct)
|
result.value = Math.min(100, roundedPct)
|
||||||
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
||||||
}
|
}
|
||||||
@@ -359,10 +328,11 @@ 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
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,6 +367,13 @@ private getAccelerationResult(numValue) {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
|
* */
|
||||||
|
def ping() {
|
||||||
|
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
||||||
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
log.debug "Refreshing Values "
|
log.debug "Refreshing Values "
|
||||||
|
|
||||||
@@ -424,13 +401,16 @@ def refresh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
sendEvent(name: "checkInterval", value: 7200, displayed: false)
|
// Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
|
||||||
|
sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
|
||||||
|
|
||||||
log.debug "Configuring Reporting"
|
log.debug "Configuring Reporting"
|
||||||
|
|
||||||
|
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||||
|
// battery minReport 30 seconds, maxReportTime 6 hrs by default
|
||||||
def configCmds = enrollResponse() +
|
def configCmds = enrollResponse() +
|
||||||
zigbee.batteryConfig() +
|
zigbee.batteryConfig() +
|
||||||
zigbee.temperatureConfig() +
|
zigbee.temperatureConfig(30, 300) +
|
||||||
zigbee.configureReporting(0xFC02, 0x0010, 0x18, 10, 3600, 0x01, [mfgCode: manufacturerCode]) +
|
zigbee.configureReporting(0xFC02, 0x0010, 0x18, 10, 3600, 0x01, [mfgCode: manufacturerCode]) +
|
||||||
zigbee.configureReporting(0xFC02, 0x0012, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) +
|
zigbee.configureReporting(0xFC02, 0x0012, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) +
|
||||||
zigbee.configureReporting(0xFC02, 0x0013, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) +
|
zigbee.configureReporting(0xFC02, 0x0013, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) +
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
//DEPRECATED - Using the smartsense-multi-sensor.groovy DTH for this device. Users need to be moved before deleting this DTH
|
//DEPRECATED - Using the smartsense-multi-sensor.groovy DTH for this device. Users need to be moved before deleting this DTH
|
||||||
|
import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
|
||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "SmartSense Open/Closed Accelerometer Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
|
definition (name: "SmartSense Open/Closed Accelerometer Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
capability "Temperature Measurement"
|
capability "Temperature Measurement"
|
||||||
capability "Health Check"
|
capability "Health Check"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
command "enrollResponse"
|
command "enrollResponse"
|
||||||
}
|
}
|
||||||
@@ -171,40 +173,9 @@ private Map parseCustomMessage(String description) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Map parseIasMessage(String description) {
|
private Map parseIasMessage(String description) {
|
||||||
List parsedMsg = description.split(' ')
|
ZoneStatus zs = zigbee.parseZoneStatus(description)
|
||||||
String msgCode = parsedMsg[2]
|
|
||||||
|
|
||||||
Map resultMap = [:]
|
return zs.isAlarm1Set() ? getContactResult('open') : getContactResult('closed')
|
||||||
switch(msgCode) {
|
|
||||||
case '0x0020': // Closed/No Motion/Dry
|
|
||||||
resultMap = getContactResult('closed')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0021': // Open/Motion/Wet
|
|
||||||
resultMap = getContactResult('open')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0022': // Tamper Alarm
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0023': // Battery Alarm
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0024': // Supervision Report
|
|
||||||
resultMap = getContactResult('closed')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0025': // Restore Report
|
|
||||||
resultMap = getContactResult('open')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0026': // Trouble/Failure
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0028': // Test Mode
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return resultMap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def getTemperature(value) {
|
def getTemperature(value) {
|
||||||
@@ -235,6 +206,8 @@ def getTemperature(value) {
|
|||||||
def maxVolts = 3.0
|
def maxVolts = 3.0
|
||||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||||
def roundedPct = Math.round(pct * 100)
|
def roundedPct = Math.round(pct * 100)
|
||||||
|
if (roundedPct <= 0)
|
||||||
|
roundedPct = 1
|
||||||
result.value = Math.min(100, roundedPct)
|
result.value = Math.min(100, roundedPct)
|
||||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||||
}
|
}
|
||||||
@@ -252,9 +225,10 @@ 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
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.st-ignore
|
||||||
|
README.md
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# 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)
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
* for the specific language governing permissions and limitations under the License.
|
* for the specific language governing permissions and limitations under the License.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
|
||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
|
definition (name: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
|
||||||
@@ -167,40 +168,8 @@ private Map parseCustomMessage(String description) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Map parseIasMessage(String description) {
|
private Map parseIasMessage(String description) {
|
||||||
List parsedMsg = description.split(' ')
|
ZoneStatus zs = zigbee.parseZoneStatus(description)
|
||||||
String msgCode = parsedMsg[2]
|
return zs.isAlarm1Set() ? getContactResult('open') : getContactResult('closed')
|
||||||
|
|
||||||
Map resultMap = [:]
|
|
||||||
switch(msgCode) {
|
|
||||||
case '0x0020': // Closed/No Motion/Dry
|
|
||||||
resultMap = getContactResult('closed')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0021': // Open/Motion/Wet
|
|
||||||
resultMap = getContactResult('open')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0022': // Tamper Alarm
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0023': // Battery Alarm
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0024': // Supervision Report
|
|
||||||
resultMap = getContactResult('closed')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0025': // Restore Report
|
|
||||||
resultMap = getContactResult('open')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0026': // Trouble/Failure
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0028': // Test Mode
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return resultMap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def getTemperature(value) {
|
def getTemperature(value) {
|
||||||
@@ -231,6 +200,8 @@ private Map getBatteryResult(rawValue) {
|
|||||||
def maxVolts = 3.0
|
def maxVolts = 3.0
|
||||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||||
def roundedPct = Math.round(pct * 100)
|
def roundedPct = Math.round(pct * 100)
|
||||||
|
if (roundedPct <= 0)
|
||||||
|
roundedPct = 1
|
||||||
result.value = Math.min(100, roundedPct)
|
result.value = Math.min(100, roundedPct)
|
||||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||||
}
|
}
|
||||||
@@ -250,7 +221,8 @@ private Map getTemperatureResult(value) {
|
|||||||
return [
|
return [
|
||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText
|
descriptionText: descriptionText,
|
||||||
|
unit: temperatureScale
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,6 +237,13 @@ private Map getContactResult(value) {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
|
* */
|
||||||
|
def ping() {
|
||||||
|
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
||||||
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
log.debug "Refreshing Temperature and Battery"
|
log.debug "Refreshing Temperature and Battery"
|
||||||
def refreshCmds = [
|
def refreshCmds = [
|
||||||
@@ -276,23 +255,19 @@ def refresh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
sendEvent(name: "checkInterval", value: 7200, displayed: false)
|
// Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
|
||||||
|
sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
|
||||||
|
|
||||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||||
def configCmds = [
|
def enrollCmds = [
|
||||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
|
|
||||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
|
||||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
|
|
||||||
]
|
]
|
||||||
return configCmds + refresh() // send refresh cmds as part of config
|
|
||||||
|
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||||
|
// battery minReport 30 seconds, maxReportTime 6 hrs by default
|
||||||
|
return enrollCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) + refresh() // send refresh cmds as part of config
|
||||||
}
|
}
|
||||||
|
|
||||||
def enrollResponse() {
|
def enrollResponse() {
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.st-ignore
|
||||||
|
README.md
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# 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)
|
||||||
@@ -21,6 +21,7 @@ metadata {
|
|||||||
capability "Temperature Measurement"
|
capability "Temperature Measurement"
|
||||||
capability "Relative Humidity Measurement"
|
capability "Relative Humidity Measurement"
|
||||||
capability "Health Check"
|
capability "Health Check"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
fingerprint endpointId: "01", inClusters: "0001,0003,0020,0402,0B05,FC45", outClusters: "0019,0003"
|
fingerprint endpointId: "01", inClusters: "0001,0003,0020,0402,0B05,FC45", outClusters: "0019,0003"
|
||||||
}
|
}
|
||||||
@@ -206,6 +207,8 @@ private Map getBatteryResult(rawValue) {
|
|||||||
def maxVolts = 3.0
|
def maxVolts = 3.0
|
||||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||||
def roundedPct = Math.round(pct * 100)
|
def roundedPct = Math.round(pct * 100)
|
||||||
|
if (roundedPct <= 0)
|
||||||
|
roundedPct = 1
|
||||||
result.value = Math.min(100, roundedPct)
|
result.value = Math.min(100, roundedPct)
|
||||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||||
}
|
}
|
||||||
@@ -225,7 +228,8 @@ private Map getTemperatureResult(value) {
|
|||||||
return [
|
return [
|
||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText
|
descriptionText: descriptionText,
|
||||||
|
unit: temperatureScale
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,6 +242,13 @@ private Map getHumidityResult(value) {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
|
* */
|
||||||
|
def ping() {
|
||||||
|
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
||||||
|
}
|
||||||
|
|
||||||
def refresh()
|
def refresh()
|
||||||
{
|
{
|
||||||
log.debug "refresh temperature, humidity, and battery"
|
log.debug "refresh temperature, humidity, and battery"
|
||||||
@@ -253,23 +264,19 @@ def refresh()
|
|||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
sendEvent(name: "checkInterval", value: 7200, displayed: false)
|
// Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
|
||||||
|
sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
|
||||||
|
|
||||||
log.debug "Configuring Reporting and Bindings."
|
log.debug "Configuring Reporting and Bindings."
|
||||||
def configCmds = [
|
def humidityConfigCmds = [
|
||||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500",
|
|
||||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
|
||||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC45 {${device.zigbeeId}} {}", "delay 500",
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC45 {${device.zigbeeId}} {}", "delay 500",
|
||||||
"zcl global send-me-a-report 0xFC45 0 0x29 30 3600 {6400}",
|
"zcl global send-me-a-report 0xFC45 0 0x29 30 3600 {6400}",
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
|
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
|
||||||
]
|
]
|
||||||
return configCmds + refresh() // send refresh cmds as part of config
|
|
||||||
|
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||||
|
// battery minReport 30 seconds, maxReportTime 6 hrs by default
|
||||||
|
return humidityConfigCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) + refresh() // send refresh cmds as part of config
|
||||||
}
|
}
|
||||||
|
|
||||||
private hex(value) {
|
private hex(value) {
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
metadata {
|
metadata {
|
||||||
definition (name: "Simulated Alarm", namespace: "smartthings/testing", author: "SmartThings") {
|
definition (name: "Simulated Alarm", namespace: "smartthings/testing", author: "SmartThings") {
|
||||||
capability "Alarm"
|
capability "Alarm"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Actuator"
|
||||||
}
|
}
|
||||||
|
|
||||||
simulator {
|
simulator {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
metadata {
|
metadata {
|
||||||
definition (name: "Simulated Color Control", namespace: "smartthings/testing", author: "SmartThings") {
|
definition (name: "Simulated Color Control", namespace: "smartthings/testing", author: "SmartThings") {
|
||||||
capability "Color Control"
|
capability "Color Control"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Actuator"
|
||||||
}
|
}
|
||||||
|
|
||||||
simulator {
|
simulator {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ metadata {
|
|||||||
// Automatically generated. Make future change here.
|
// Automatically generated. Make future change here.
|
||||||
definition (name: "Simulated Contact Sensor", namespace: "smartthings/testing", author: "bob") {
|
definition (name: "Simulated Contact Sensor", namespace: "smartthings/testing", author: "bob") {
|
||||||
capability "Contact Sensor"
|
capability "Contact Sensor"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
command "open"
|
command "open"
|
||||||
command "close"
|
command "close"
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ metadata {
|
|||||||
// Automatically generated. Make future change here.
|
// Automatically generated. Make future change here.
|
||||||
definition (name: "Simulated Lock", namespace: "smartthings/testing", author: "bob") {
|
definition (name: "Simulated Lock", namespace: "smartthings/testing", author: "bob") {
|
||||||
capability "Lock"
|
capability "Lock"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Actuator"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simulated lock
|
// Simulated lock
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ metadata {
|
|||||||
// Automatically generated. Make future change here.
|
// Automatically generated. Make future change here.
|
||||||
definition (name: "Simulated Motion Sensor", namespace: "smartthings/testing", author: "bob") {
|
definition (name: "Simulated Motion Sensor", namespace: "smartthings/testing", author: "bob") {
|
||||||
capability "Motion Sensor"
|
capability "Motion Sensor"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
command "active"
|
command "active"
|
||||||
command "inactive"
|
command "inactive"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ metadata {
|
|||||||
// Automatically generated. Make future change here.
|
// Automatically generated. Make future change here.
|
||||||
definition (name: "Simulated Presence Sensor", namespace: "smartthings/testing", author: "bob") {
|
definition (name: "Simulated Presence Sensor", namespace: "smartthings/testing", author: "bob") {
|
||||||
capability "Presence Sensor"
|
capability "Presence Sensor"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
command "arrived"
|
command "arrived"
|
||||||
command "departed"
|
command "departed"
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ metadata {
|
|||||||
definition (name: "Simulated Switch", namespace: "smartthings/testing", author: "bob") {
|
definition (name: "Simulated Switch", namespace: "smartthings/testing", author: "bob") {
|
||||||
capability "Switch"
|
capability "Switch"
|
||||||
capability "Relay Switch"
|
capability "Relay Switch"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Actuator"
|
||||||
|
|
||||||
command "onPhysical"
|
command "onPhysical"
|
||||||
command "offPhysical"
|
command "offPhysical"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ metadata {
|
|||||||
definition (name: "Simulated Temperature Sensor", namespace: "smartthings/testing", author: "SmartThings") {
|
definition (name: "Simulated Temperature Sensor", namespace: "smartthings/testing", author: "SmartThings") {
|
||||||
capability "Temperature Measurement"
|
capability "Temperature Measurement"
|
||||||
capability "Switch Level"
|
capability "Switch Level"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
command "up"
|
command "up"
|
||||||
command "down"
|
command "down"
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ metadata {
|
|||||||
definition (name: "Simulated Thermostat", namespace: "smartthings/testing", author: "SmartThings") {
|
definition (name: "Simulated Thermostat", namespace: "smartthings/testing", author: "SmartThings") {
|
||||||
capability "Thermostat"
|
capability "Thermostat"
|
||||||
capability "Relative Humidity Measurement"
|
capability "Relative Humidity Measurement"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Actuator"
|
||||||
|
|
||||||
command "tempUp"
|
command "tempUp"
|
||||||
command "tempDown"
|
command "tempDown"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ metadata {
|
|||||||
// Automatically generated. Make future change here.
|
// Automatically generated. Make future change here.
|
||||||
definition (name: "Simulated Water Sensor", namespace: "smartthings/testing", author: "SmartThings") {
|
definition (name: "Simulated Water Sensor", namespace: "smartthings/testing", author: "SmartThings") {
|
||||||
capability "Water Sensor"
|
capability "Water Sensor"
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
command "wet"
|
command "wet"
|
||||||
command "dry"
|
command "dry"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
* for the specific language governing permissions and limitations under the License.
|
* for the specific language governing permissions and limitations under the License.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
|
||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "Tyco Door/Window Sensor", namespace: "smartthings", author: "SmartThings") {
|
definition (name: "Tyco Door/Window Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||||
@@ -161,40 +162,9 @@ private Map parseCustomMessage(String description) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Map parseIasMessage(String description) {
|
private Map parseIasMessage(String description) {
|
||||||
List parsedMsg = description.split(' ')
|
ZoneStatus zs = zigbee.parseZoneStatus(description)
|
||||||
String msgCode = parsedMsg[2]
|
|
||||||
|
|
||||||
Map resultMap = [:]
|
return zs.isAlarm1Set() ? getContactResult('open') : getContactResult('closed')
|
||||||
switch(msgCode) {
|
|
||||||
case '0x0020': // Closed/No Motion/Dry
|
|
||||||
resultMap = getContactResult('closed')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0021': // Open/Motion/Wet
|
|
||||||
resultMap = getContactResult('open')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0022': // Tamper Alarm
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0023': // Battery Alarm
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0024': // Supervision Report
|
|
||||||
resultMap = getContactResult('closed')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0025': // Restore Report
|
|
||||||
resultMap = getContactResult('open')
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0026': // Trouble/Failure
|
|
||||||
break
|
|
||||||
|
|
||||||
case '0x0028': // Test Mode
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return resultMap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def getTemperature(value) {
|
def getTemperature(value) {
|
||||||
@@ -243,7 +213,8 @@ private Map getTemperatureResult(value) {
|
|||||||
return [
|
return [
|
||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText
|
descriptionText: descriptionText,
|
||||||
|
unit: temperatureScale
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Iris Smart Fob
|
* ZigBee Button
|
||||||
*
|
*
|
||||||
* Copyright 2015 Mitch Pond
|
* Copyright 2015 Mitch Pond
|
||||||
* Presence code adapted from SmartThings Arrival Sensor HA device type
|
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
* 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:
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
@@ -14,181 +13,235 @@
|
|||||||
* for the specific language governing permissions and limitations under the License.
|
* for the specific language governing permissions and limitations under the License.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "ZigBee Button", namespace: "smartthings", author: "Mitch Pond") {
|
definition (name: "ZigBee Button", namespace: "smartthings", author: "Mitch Pond") {
|
||||||
capability "Battery"
|
capability "Actuator"
|
||||||
capability "Button"
|
capability "Battery"
|
||||||
|
capability "Button"
|
||||||
capability "Configuration"
|
capability "Configuration"
|
||||||
capability "Presence Sensor"
|
capability "Refresh"
|
||||||
capability "Sensor"
|
capability "Sensor"
|
||||||
|
|
||||||
//fingerprint endpointId: "01", profileId: "0104", inClusters: "0000,0001,0003,0007,0020,0B05", outClusters: "0003,0006,0019", model:"3450-L", manufacturer: "CentraLite"
|
command "enrollResponse"
|
||||||
}
|
|
||||||
|
fingerprint inClusters: "0000, 0001, 0003, 0020, 0402, 0B05", outClusters: "0003, 0006, 0008, 0019", manufacturer: "OSRAM", model: "LIGHTIFY Dimming Switch", deviceJoinName: "OSRAM LIGHTIFY Dimming Switch"
|
||||||
preferences{
|
//fingerprint inClusters: "0000, 0001, 0003, 0020, 0500", outClusters: "0003,0019", manufacturer: "CentraLite", model: "3455-L", deviceJoinName: "Iris Care Pendant"
|
||||||
input ("holdTime", "number", title: "Minimum time in seconds for a press to count as \"held\"",
|
//fingerprint inClusters: "0000, 0001, 0003, 0007, 0020, 0402, 0B05", outClusters: "0003, 0006, 0019", manufacturer: "CentraLite", model: "3460-L", deviceJoinName: "Iris Smart Button"
|
||||||
defaultValue: 3, displayDuringSetup: false)
|
//fingerprint inClusters: "0000, 0001, 0003, 0007, 0020, 0B05", outClusters: "0003, 0006, 0019", manufacturer: "CentraLite", model:"3450-L", deviceJoinName: "Iris KeyFob"
|
||||||
input "checkInterval", "enum", title: "Presence timeout (minutes)",
|
|
||||||
defaultValue:"2", options: ["2", "3", "5"], displayDuringSetup: false
|
|
||||||
input "logging", "bool", title: "Enable debug logging",
|
|
||||||
defaultValue: false, displayDuringSetup: false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles(scale: 2) {
|
simulator {}
|
||||||
standardTile("presence", "device.presence", width: 4, height: 4, canChangeBackground: true) {
|
|
||||||
state "present", label: "Present", labelIcon:"st.presence.tile.present", backgroundColor:"#53a7c0"
|
|
||||||
state "not present", labelIcon:"st.presence.tile.not-present", backgroundColor:"#ffffff"
|
|
||||||
}
|
|
||||||
standardTile("button", "device.button", decoration: "flat", width: 2, height: 2) {
|
|
||||||
state "default", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff"
|
|
||||||
}
|
|
||||||
valueTile("battery", "device.battery", decoration: "flat", width: 2, height: 2) {
|
|
||||||
state "battery", label:'${currentValue}% battery', unit:""
|
|
||||||
}
|
|
||||||
|
|
||||||
main (["presence"])
|
preferences {
|
||||||
details(["presence","button","battery"])
|
section {
|
||||||
}
|
input ("holdTime", "number", title: "Minimum time in seconds for a press to count as \"held\"", defaultValue: 1, displayDuringSetup: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles {
|
||||||
|
standardTile("button", "device.button", width: 2, height: 2) {
|
||||||
|
state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff"
|
||||||
|
state "button 1 pushed", label: "pushed #1", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#79b821"
|
||||||
|
}
|
||||||
|
|
||||||
|
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
main (["button"])
|
||||||
|
details(["button", "battery", "refresh"])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse(String description) {
|
def parse(String description) {
|
||||||
def descMap = zigbee.parseDescriptionAsMap(description)
|
log.debug "description is $description"
|
||||||
logIt descMap
|
def event = zigbee.getEvent(description)
|
||||||
state.lastCheckin = now()
|
if (event) {
|
||||||
logIt "lastCheckin = ${state.lastCheckin}"
|
sendEvent(event)
|
||||||
handlePresenceEvent(true)
|
}
|
||||||
|
else {
|
||||||
def results = []
|
if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) {
|
||||||
if (description?.startsWith('catchall:'))
|
def descMap = zigbee.parseDescriptionAsMap(description)
|
||||||
results = parseCatchAllMessage(descMap)
|
if (descMap.clusterInt == 0x0001 && descMap.attrInt == 0x0020) {
|
||||||
else if (description?.startsWith('read attr -'))
|
event = getBatteryResult(zigbee.convertHexToInt(descMap.value))
|
||||||
results = parseReportAttributeMessage(descMap)
|
}
|
||||||
else logIt(descMap, "trace")
|
else if (descMap.clusterInt == 0x0006 || descMap.clusterInt == 0x0008) {
|
||||||
|
event = parseNonIasButtonMessage(descMap)
|
||||||
return results;
|
}
|
||||||
|
}
|
||||||
|
else if (description?.startsWith('zone status')) {
|
||||||
|
event = parseIasButtonMessage(description)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug "Parse returned $event"
|
||||||
|
def result = event ? createEvent(event) : []
|
||||||
|
|
||||||
|
if (description?.startsWith('enroll request')) {
|
||||||
|
List cmds = enrollResponse()
|
||||||
|
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseIasButtonMessage(String description) {
|
||||||
|
int zoneInt = Integer.parseInt((description - "zone status 0x"), 16)
|
||||||
|
if (zoneInt & 0x02) {
|
||||||
|
resultMap = getButtonResult('press')
|
||||||
|
} else {
|
||||||
|
resultMap = getButtonResult('release')
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map getBatteryResult(rawValue) {
|
||||||
|
log.debug 'Battery'
|
||||||
|
def volts = rawValue / 10
|
||||||
|
if (volts > 3.0 || volts == 0 || rawValue == 0xFF) {
|
||||||
|
return [:]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
def result = [
|
||||||
|
name: 'battery'
|
||||||
|
]
|
||||||
|
def minVolts = 2.1
|
||||||
|
def maxVolts = 3.0
|
||||||
|
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||||
|
result.value = Math.min(100, (int) pct * 100)
|
||||||
|
def linkText = getLinkText(device)
|
||||||
|
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map parseNonIasButtonMessage(Map descMap){
|
||||||
|
def buttonState = ""
|
||||||
|
def buttonNumber = 0
|
||||||
|
if (((device.getDataValue("model") == "3460-L") || (device.getDataValue("model") == "3450-L"))
|
||||||
|
&&(descMap.clusterInt == 0x0006)) {
|
||||||
|
if (descMap.command == "01") {
|
||||||
|
getButtonResult("press")
|
||||||
|
}
|
||||||
|
else if (descMap.command == "00") {
|
||||||
|
getButtonResult("release")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (descMap.clusterInt == 0x0006) {
|
||||||
|
buttonState = "pushed"
|
||||||
|
if (descMap.command == "01") {
|
||||||
|
buttonNumber = 1
|
||||||
|
}
|
||||||
|
else if (descMap.command == "00") {
|
||||||
|
buttonNumber = 2
|
||||||
|
}
|
||||||
|
if (buttonNumber !=0) {
|
||||||
|
def descriptionText = "$device.displayName button $buttonNumber was $buttonState"
|
||||||
|
return createEvent(name: "button", value: buttonState, data: [buttonNumber: buttonNumber], descriptionText: descriptionText, isStateChange: true)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return [:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (descMap.clusterInt == 0x0008) {
|
||||||
|
if (descMap.command == "05") {
|
||||||
|
state.buttonNumber = 1
|
||||||
|
getButtonResult("press", 1)
|
||||||
|
}
|
||||||
|
else if (descMap.command == "01") {
|
||||||
|
state.buttonNumber = 2
|
||||||
|
getButtonResult("press", 2)
|
||||||
|
}
|
||||||
|
else if (descMap.command == "03") {
|
||||||
|
getButtonResult("release", state.buttonNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
log.debug "Refreshing Battery"
|
||||||
|
|
||||||
|
return zigbee.readAttribute(0x0001, 0x20) +
|
||||||
|
zigbee.enrollResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||||
|
def cmds = []
|
||||||
|
if (device.getDataValue("model") == "3450-L") {
|
||||||
|
cmds << [
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 300",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 2 1 6 {${device.zigbeeId}} {}", "delay 300",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 3 1 6 {${device.zigbeeId}} {}", "delay 300",
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 4 1 6 {${device.zigbeeId}} {}", "delay 300"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return zigbee.onOffConfig() +
|
||||||
|
zigbee.levelConfig() +
|
||||||
|
zigbee.configureReporting(0x0001, 0x20, 0x20, 30, 21600, 0x01) +
|
||||||
|
zigbee.enrollResponse() +
|
||||||
|
zigbee.readAttribute(0x0001, 0x20) +
|
||||||
|
cmds
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map getButtonResult(buttonState, buttonNumber = 1) {
|
||||||
|
if (buttonState == 'release') {
|
||||||
|
log.debug "Button was value : $buttonState"
|
||||||
|
def timeDiff = now() - state.pressTime
|
||||||
|
log.info "timeDiff: $timeDiff"
|
||||||
|
def holdPreference = holdTime ?: 1
|
||||||
|
log.info "holdp1 : $holdPreference"
|
||||||
|
holdPreference = (holdPreference as int) * 1000
|
||||||
|
log.info "holdp2 : $holdPreference"
|
||||||
|
if (timeDiff > 10000) { //timeDiff>10sec check for refresh sending release value causing actions to be executed
|
||||||
|
return [:]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (timeDiff < holdPreference) {
|
||||||
|
buttonState = "pushed"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
buttonState = "held"
|
||||||
|
}
|
||||||
|
def descriptionText = "$device.displayName button $buttonNumber was $buttonState"
|
||||||
|
return createEvent(name: "button", value: buttonState, data: [buttonNumber: buttonNumber], descriptionText: descriptionText, isStateChange: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (buttonState == 'press') {
|
||||||
|
log.debug "Button was value : $buttonState"
|
||||||
|
state.pressTime = now()
|
||||||
|
log.info "presstime: ${state.pressTime}"
|
||||||
|
return [:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def installed() {
|
||||||
|
initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
def updated() {
|
def updated() {
|
||||||
startTimer()
|
initialize()
|
||||||
configure()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure(){
|
def initialize() {
|
||||||
logIt "Configuring Smart Fob..."
|
if ((device.getDataValue("manufacturer") == "OSRAM") && (device.getDataValue("model") == "LIGHTIFY Dimming Switch")) {
|
||||||
[
|
sendEvent(name: "numberOfButtons", value: 2)
|
||||||
"zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 200",
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} 2 1 6 {${device.zigbeeId}} {}", "delay 200",
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} 3 1 6 {${device.zigbeeId}} {}", "delay 200",
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} 4 1 6 {${device.zigbeeId}} {}", "delay 200",
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 200"
|
|
||||||
] +
|
|
||||||
zigbee.configureReporting(0x0001,0x0020,0x20,20,20,0x01)
|
|
||||||
}
|
|
||||||
|
|
||||||
def parseCatchAllMessage(descMap) {
|
|
||||||
if (descMap?.clusterId == "0006" && descMap?.command == "01") //button pressed
|
|
||||||
handleButtonPress(descMap.sourceEndpoint as int)
|
|
||||||
else if (descMap?.clusterId == "0006" && descMap?.command == "00") //button released
|
|
||||||
handleButtonRelease(descMap.sourceEndpoint as int)
|
|
||||||
else logIt("Parse: Unhandled message: ${descMap}","trace")
|
|
||||||
}
|
|
||||||
|
|
||||||
def parseReportAttributeMessage(descMap) {
|
|
||||||
if (descMap?.cluster == "0001" && descMap?.attrId == "0020") createBatteryEvent(getBatteryLevel(descMap.value))
|
|
||||||
else logIt descMap
|
|
||||||
}
|
|
||||||
|
|
||||||
private createBatteryEvent(percent) {
|
|
||||||
logIt "Battery level at " + percent
|
|
||||||
return createEvent([name: "battery", value: percent])
|
|
||||||
}
|
|
||||||
|
|
||||||
//this method determines if a press should count as a push or a hold and returns the relevant event type
|
|
||||||
private handleButtonRelease(button) {
|
|
||||||
logIt "lastPress state variable: ${state.lastPress}"
|
|
||||||
def sequenceError = {logIt("Uh oh...missed a message? Dropping this event.", "error"); state.lastPress = null; return []}
|
|
||||||
|
|
||||||
if (!state.lastPress) return sequenceError()
|
|
||||||
else if (state.lastPress.button != button) return sequenceError()
|
|
||||||
|
|
||||||
def currentTime = now()
|
|
||||||
def startOfPress = state.lastPress?.time
|
|
||||||
def timeDif = currentTime - startOfPress
|
|
||||||
def holdTimeMillisec = (settings.holdTime?:3).toInteger() * 1000
|
|
||||||
|
|
||||||
state.lastPress = null //we're done with this. clear it to make error conditions easier to catch
|
|
||||||
|
|
||||||
if (timeDif < 0)
|
|
||||||
//likely a message sequence issue or dropped packet. Drop this press and wait for another.
|
|
||||||
return sequenceError()
|
|
||||||
else if (timeDif < holdTimeMillisec)
|
|
||||||
return createButtonEvent(button,"pushed")
|
|
||||||
else
|
|
||||||
return createButtonEvent(button,"held")
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleButtonPress(button) {
|
|
||||||
state.lastPress = [button: button, time: now()]
|
|
||||||
}
|
|
||||||
|
|
||||||
private createButtonEvent(button,action) {
|
|
||||||
logIt "Button ${button} ${action}"
|
|
||||||
return createEvent([
|
|
||||||
name: "button",
|
|
||||||
value: action,
|
|
||||||
data:[buttonNumber: button],
|
|
||||||
descriptionText: "${device.displayName} button ${button} was ${action}",
|
|
||||||
isStateChange: true,
|
|
||||||
displayed: true])
|
|
||||||
}
|
|
||||||
|
|
||||||
private getBatteryLevel(rawValue) {
|
|
||||||
def intValue = Integer.parseInt(rawValue,16)
|
|
||||||
def min = 2.1
|
|
||||||
def max = 3.0
|
|
||||||
def vBatt = intValue / 10
|
|
||||||
return ((vBatt - min) / (max - min) * 100) as int
|
|
||||||
}
|
|
||||||
|
|
||||||
private handlePresenceEvent(present) {
|
|
||||||
def wasPresent = device.currentState("presence")?.value == "present"
|
|
||||||
if (!wasPresent && present) {
|
|
||||||
logIt "Sensor is present"
|
|
||||||
startTimer()
|
|
||||||
} else if (!present) {
|
|
||||||
logIt "Sensor is not present"
|
|
||||||
stopTimer()
|
|
||||||
}
|
}
|
||||||
def linkText = getLinkText(device)
|
else if ((device.getDataValue("manufacturer") == "CentraLite") &&
|
||||||
def eventMap = [
|
((device.getDataValue("model") == "3455-L") || (device.getDataValue("model") == "3460-L"))) {
|
||||||
name: "presence",
|
sendEvent(name: "numberOfButtons", value: 1)
|
||||||
value: present ? "present" : "not present",
|
|
||||||
linkText: linkText,
|
|
||||||
descriptionText: "${linkText} has ${present ? 'arrived' : 'left'}",
|
|
||||||
]
|
|
||||||
logIt "Creating presence event: ${eventMap}"
|
|
||||||
sendEvent(eventMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
private startTimer() {
|
|
||||||
logIt "Scheduling periodic timer"
|
|
||||||
schedule("0 * * * * ?", checkPresenceCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
private stopTimer() {
|
|
||||||
logIt "Stopping periodic timer"
|
|
||||||
unschedule()
|
|
||||||
}
|
|
||||||
|
|
||||||
def checkPresenceCallback() {
|
|
||||||
def timeSinceLastCheckin = (now() - state.lastCheckin) / 1000
|
|
||||||
def theCheckInterval = (checkInterval ? checkInterval as int : 2) * 60
|
|
||||||
logIt "Sensor checked in ${timeSinceLastCheckin} seconds ago"
|
|
||||||
if (timeSinceLastCheckin >= theCheckInterval) {
|
|
||||||
handlePresenceEvent(false)
|
|
||||||
}
|
}
|
||||||
|
else if ((device.getDataValue("manufacturer") == "CentraLite") && (device.getDataValue("model") == "3450-L")) {
|
||||||
|
sendEvent(name: "numberOfButtons", value: 4)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//default. can be changed
|
||||||
|
sendEvent(name: "numberOfButtons", value: 4)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ****** Utility functions ******
|
|
||||||
|
|
||||||
private logIt(str, logLevel = 'debug') {if (settings.logging) log."$logLevel"(str) }
|
|
||||||
@@ -19,6 +19,7 @@ metadata {
|
|||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
capability "Switch"
|
capability "Switch"
|
||||||
capability "Switch Level"
|
capability "Switch Level"
|
||||||
|
capability "Health Check"
|
||||||
|
|
||||||
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008"
|
||||||
@@ -53,7 +54,10 @@ def parse(String description) {
|
|||||||
|
|
||||||
def event = zigbee.getEvent(description)
|
def event = zigbee.getEvent(description)
|
||||||
if (event) {
|
if (event) {
|
||||||
sendEvent(event)
|
if (event.name=="level" && event.value==0) {}
|
||||||
|
else {
|
||||||
|
sendEvent(event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||||
@@ -72,6 +76,12 @@ def on() {
|
|||||||
def setLevel(value) {
|
def setLevel(value) {
|
||||||
zigbee.setLevel(value)
|
zigbee.setLevel(value)
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
|
* */
|
||||||
|
def ping() {
|
||||||
|
return zigbee.onOffRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig()
|
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig()
|
||||||
@@ -79,5 +89,8 @@ def refresh() {
|
|||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
log.debug "Configuring Reporting and Bindings."
|
log.debug "Configuring Reporting and Bindings."
|
||||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
|
// Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
|
||||||
|
sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
|
||||||
|
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||||
|
zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ metadata {
|
|||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
capability "Switch"
|
capability "Switch"
|
||||||
capability "Switch Level"
|
capability "Switch Level"
|
||||||
|
capability "Health Check"
|
||||||
|
|
||||||
|
attribute "colorName", "string"
|
||||||
|
command "setGenericName"
|
||||||
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Flex RGBW", deviceJoinName: "OSRAM LIGHTIFY LED FLEXIBLE STRIP RGBW"
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Flex RGBW", deviceJoinName: "OSRAM LIGHTIFY LED FLEXIBLE STRIP RGBW"
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW", deviceJoinName: "OSRAM LIGHTIFY LED FLEXIBLE STRIP RGBW"
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW", deviceJoinName: "OSRAM LIGHTIFY LED FLEXIBLE STRIP RGBW"
|
||||||
@@ -54,15 +58,15 @@ metadata {
|
|||||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") {
|
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") {
|
||||||
state "colorTemperature", action:"color temperature.setColorTemperature"
|
state "colorTemperature", action:"color temperature.setColorTemperature"
|
||||||
}
|
}
|
||||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||||
state "colorTemperature", label: '${currentValue} K'
|
state "colorName", label: '${currentValue}'
|
||||||
}
|
}
|
||||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
}
|
}
|
||||||
|
|
||||||
main(["switch"])
|
main(["switch"])
|
||||||
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
|
details(["switch", "colorTempSliderControl", "colorName", "refresh"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,10 +82,16 @@ private getATTRIBUTE_COLOR_TEMPERATURE() { 0x0007 }
|
|||||||
def parse(String description) {
|
def parse(String description) {
|
||||||
log.debug "description is $description"
|
log.debug "description is $description"
|
||||||
|
|
||||||
def finalResult = zigbee.getEvent(description)
|
def event = zigbee.getEvent(description)
|
||||||
if (finalResult) {
|
if (event) {
|
||||||
log.debug finalResult
|
log.debug event
|
||||||
sendEvent(finalResult)
|
if (event.name=="level" && event.value==0) {}
|
||||||
|
else {
|
||||||
|
if (event.name=="colorTemperature") {
|
||||||
|
setGenericName(event.value)
|
||||||
|
}
|
||||||
|
sendEvent(event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
def zigbeeMap = zigbee.parseDescriptionAsMap(description)
|
def zigbeeMap = zigbee.parseDescriptionAsMap(description)
|
||||||
@@ -89,12 +99,12 @@ def parse(String description) {
|
|||||||
|
|
||||||
if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) {
|
if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) {
|
||||||
if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute
|
if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute
|
||||||
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 360)
|
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||||
sendEvent(name: "hue", value: hueValue, displayed:false)
|
sendEvent(name: "hue", value: hueValue, descriptionText: "Color has changed")
|
||||||
}
|
}
|
||||||
else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute
|
else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute
|
||||||
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||||
sendEvent(name: "saturation", value: saturationValue, displayed:false)
|
sendEvent(name: "saturation", value: saturationValue, descriptionText: "Color has changed", displayed: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -110,20 +120,47 @@ def on() {
|
|||||||
def off() {
|
def off() {
|
||||||
zigbee.off()
|
zigbee.off()
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
|
* */
|
||||||
|
def ping() {
|
||||||
|
return zigbee.onOffRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
zigbee.readAttribute(0x0006, 0x00) + zigbee.readAttribute(0x0008, 0x00) + zigbee.readAttribute(0x0300, 0x00) + zigbee.readAttribute(0x0300, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(0x0300, ATTRIBUTE_HUE) + zigbee.readAttribute(0x0300, ATTRIBUTE_SATURATION) + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01)
|
zigbee.onOffRefresh() + zigbee.readAttribute(0x0008, 0x00) + zigbee.readAttribute(0x0300, 0x00) + zigbee.readAttribute(0x0300, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(0x0300, ATTRIBUTE_HUE) + zigbee.readAttribute(0x0300, ATTRIBUTE_SATURATION) + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01)
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
log.debug "Configuring Reporting and Bindings."
|
log.debug "Configuring Reporting and Bindings."
|
||||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) + zigbee.readAttribute(0x0006, 0x00) + zigbee.readAttribute(0x0008, 0x00) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, 0x00) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
// Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
|
||||||
|
sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
|
||||||
|
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||||
|
zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) + zigbee.readAttribute(0x0006, 0x00) + zigbee.readAttribute(0x0008, 0x00) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, 0x00) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||||
}
|
}
|
||||||
|
|
||||||
def setColorTemperature(value) {
|
def setColorTemperature(value) {
|
||||||
|
setGenericName(value)
|
||||||
zigbee.setColorTemperature(value)
|
zigbee.setColorTemperature(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature
|
||||||
|
def setGenericName(value){
|
||||||
|
if (value != null) {
|
||||||
|
def genericName = "White"
|
||||||
|
if (value < 3300) {
|
||||||
|
genericName = "Soft White"
|
||||||
|
} else if (value < 4150) {
|
||||||
|
genericName = "Moonlight"
|
||||||
|
} else if (value <= 5000) {
|
||||||
|
genericName = "Cool White"
|
||||||
|
} else if (value >= 5000) {
|
||||||
|
genericName = "Daylight"
|
||||||
|
}
|
||||||
|
sendEvent(name: "colorName", value: genericName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def setLevel(value) {
|
def setLevel(value) {
|
||||||
zigbee.setLevel(value)
|
zigbee.setLevel(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright 2015 SmartThings
|
* Copyright 2016 SmartThings
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
* 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:
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
@@ -11,100 +11,133 @@
|
|||||||
* for the specific language governing permissions and limitations under the License.
|
* for the specific language governing permissions and limitations under the License.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
/*
|
|
||||||
* Capabilities
|
|
||||||
* - Battery
|
|
||||||
* - Configuration
|
|
||||||
* - Refresh
|
|
||||||
* - Switch
|
|
||||||
* - Valve
|
|
||||||
*/
|
|
||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "Zigbee Valve", namespace: "smartthings", author: "SmartThings") {
|
definition (name: "ZigBee Valve", namespace: "smartthings", author: "SmartThings") {
|
||||||
capability "Battery"
|
capability "Actuator"
|
||||||
capability "Configuration"
|
capability "Battery"
|
||||||
capability "Refresh"
|
capability "Configuration"
|
||||||
capability "Switch"
|
capability "Power Source"
|
||||||
capability "Valve"
|
capability "Refresh"
|
||||||
|
capability "Valve"
|
||||||
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0020,0006,0B02", outClusters: "0003"
|
fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0006, 0020, 0B02, FC02", outClusters: "0019", manufacturer: "WAXMAN", model: "leakSMART Water Valve v2.10", deviceJoinName: "leakSMART Valve"
|
||||||
}
|
fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0004, 0005, 0006, 0008, 000F, 0020, 0B02", outClusters: "0003, 0019", manufacturer: "WAXMAN", model: "House Water Valve - MDL-TBD", deviceJoinName: "Waxman House Water Valve"
|
||||||
|
}
|
||||||
|
|
||||||
// simulator metadata
|
// simulator metadata
|
||||||
simulator {
|
simulator {
|
||||||
// status messages
|
// status messages
|
||||||
status "on": "on/off: 1"
|
status "on": "on/off: 1"
|
||||||
status "off": "on/off: 0"
|
status "off": "on/off: 0"
|
||||||
|
|
||||||
// reply messages
|
// reply messages
|
||||||
reply "zcl on-off on": "on/off: 1"
|
reply "zcl on-off on": "on/off: 1"
|
||||||
reply "zcl on-off off": "on/off: 0"
|
reply "zcl on-off off": "on/off: 0"
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI tile definitions
|
tiles(scale: 2) {
|
||||||
tiles {
|
multiAttributeTile(name:"valve", type: "generic", width: 6, height: 4, canChangeIcon: true){
|
||||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
tileAttribute ("device.contact", key: "PRIMARY_CONTROL") {
|
||||||
state "off", label: 'closed', action: "switch.on", icon: "st.Outdoor.outdoor16", backgroundColor: "#e86d13"
|
attributeState "open", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#53a7c0", nextState:"closing"
|
||||||
state "on", label: 'open', action: "switch.off", icon: "st.Outdoor.outdoor16", backgroundColor: "#53a7c0"
|
attributeState "closed", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#e86d13", nextState:"opening"
|
||||||
}
|
attributeState "opening", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#53a7c0", nextState:"closing"
|
||||||
main "switch"
|
attributeState "closing", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#e86d13", nextState:"opening"
|
||||||
details(["switch"])
|
}
|
||||||
}
|
tileAttribute ("powerSource", key: "SECONDARY_CONTROL") {
|
||||||
|
attributeState "powerSource", label:'Power Source: ${currentValue}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
valueTile("battery", "device.battery", inactiveLabel:false, decoration:"flat", width:2, height:2) {
|
||||||
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["valve"])
|
||||||
|
details(["valve", "battery", "refresh"])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getCLUSTER_BASIC() { 0x0000 }
|
||||||
|
private getBASIC_ATTR_POWER_SOURCE() { 0x0007 }
|
||||||
|
private getCLUSTER_POWER() { 0x0001 }
|
||||||
|
private getPOWER_ATTR_BATTERY_PERCENTAGE_REMAINING() { 0x0021 }
|
||||||
|
private getTYPE_U8() { 0x20 }
|
||||||
|
private getTYPE_ENUM8() { 0x30 }
|
||||||
|
|
||||||
// Parse incoming device messages to generate events
|
// Parse incoming device messages to generate events
|
||||||
def parse(String description) {
|
def parse(String description) {
|
||||||
log.info description
|
log.debug "description is $description"
|
||||||
if (description?.startsWith("catchall:")) {
|
def event = zigbee.getEvent(description)
|
||||||
def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null
|
if (event) {
|
||||||
def result = createEvent(name: name, value: value)
|
if(event.name == "switch") {
|
||||||
def msg = zigbee.parse(description)
|
event.name = "contact" //0006 cluster in valve is tied to contact
|
||||||
log.debug "Parse returned ${result?.descriptionText}"
|
if(event.value == "on") {
|
||||||
return result
|
event.value = "open"
|
||||||
log.trace msg
|
}
|
||||||
log.trace "data: $msg.data"
|
else if(event.value == "off") {
|
||||||
}
|
event.value = "closed"
|
||||||
else {
|
}
|
||||||
def name = description?.startsWith("on/off: ") ? "switch" : null
|
}
|
||||||
def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null
|
sendEvent(event)
|
||||||
def result = createEvent(name: name, value: value)
|
}
|
||||||
log.debug "Parse returned ${result?.descriptionText}"
|
else {
|
||||||
return result
|
def descMap = zigbee.parseDescriptionAsMap(description)
|
||||||
}
|
if (descMap.clusterInt == CLUSTER_BASIC && descMap.attrInt == BASIC_ATTR_POWER_SOURCE){
|
||||||
}
|
def value = descMap.value
|
||||||
|
if (value == "01" || value == "02") {
|
||||||
// Commands to device
|
sendEvent(name: "powerSource", value: "Mains")
|
||||||
def on() {
|
}
|
||||||
log.debug "on()"
|
else if (value == "03") {
|
||||||
sendEvent(name: "switch", value: "on")
|
sendEvent(name: "powerSource", value: "Battery")
|
||||||
"st cmd 0x${device.deviceNetworkId} 1 6 1 {}"
|
}
|
||||||
}
|
else if (value == "04") {
|
||||||
|
sendEvent(name: "powerSource", value: "DC")
|
||||||
def off() {
|
}
|
||||||
log.debug "off()"
|
else {
|
||||||
sendEvent(name: "switch", value: "off")
|
sendEvent(name: "powerSource", value: "Unknown")
|
||||||
"st cmd 0x${device.deviceNetworkId} 1 6 0 {}"
|
}
|
||||||
|
}
|
||||||
|
else if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) {
|
||||||
|
event.name = "battery"
|
||||||
|
event.value = Math.round(Integer.parseInt(descMap.value, 16) / 2)
|
||||||
|
sendEvent(event)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||||
|
log.debug descMap
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def open() {
|
def open() {
|
||||||
log.debug "on()"
|
zigbee.on()
|
||||||
sendEvent(name: "switch", value: "on")
|
|
||||||
"st cmd 0x${device.deviceNetworkId} 1 6 1 {}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def close() {
|
def close() {
|
||||||
log.debug "off()"
|
zigbee.off()
|
||||||
sendEvent(name: "switch", value: "off")
|
|
||||||
"st cmd 0x${device.deviceNetworkId} 1 6 0 {}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
log.debug "sending refresh command"
|
log.debug "refresh called"
|
||||||
"st rattr 0x${device.deviceNetworkId} 1 6 0"
|
zigbee.onOffRefresh() +
|
||||||
|
zigbee.readAttribute(CLUSTER_BASIC, BASIC_ATTR_POWER_SOURCE) +
|
||||||
|
zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) +
|
||||||
|
zigbee.onOffConfig() +
|
||||||
|
zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING, TYPE_U8, 600, 21600, 1) +
|
||||||
|
zigbee.configureReporting(CLUSTER_BASIC, BASIC_ATTR_POWER_SOURCE, TYPE_ENUM8, 5, 21600, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
|
log.debug "Configuring Reporting and Bindings."
|
||||||
"zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}"
|
zigbee.onOffConfig() +
|
||||||
|
zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING, TYPE_U8, 600, 21600, 1) +
|
||||||
|
zigbee.configureReporting(CLUSTER_BASIC, BASIC_ATTR_POWER_SOURCE, TYPE_ENUM8, 5, 21600, 1) +
|
||||||
|
zigbee.onOffRefresh() +
|
||||||
|
zigbee.readAttribute(CLUSTER_BASIC, BASIC_ATTR_POWER_SOURCE) +
|
||||||
|
zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ metadata {
|
|||||||
capability "Actuator"
|
capability "Actuator"
|
||||||
capability "Color Temperature"
|
capability "Color Temperature"
|
||||||
capability "Configuration"
|
capability "Configuration"
|
||||||
|
capability "Health Check"
|
||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
capability "Switch"
|
capability "Switch"
|
||||||
capability "Switch Level"
|
capability "Switch Level"
|
||||||
@@ -35,6 +36,7 @@ metadata {
|
|||||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY RT Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Recessed Kit RT 5/6 Tunable White"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "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
|
||||||
@@ -49,9 +51,6 @@ metadata {
|
|||||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||||
attributeState "level", action:"switch level.setLevel"
|
attributeState "level", action:"switch level.setLevel"
|
||||||
}
|
}
|
||||||
tileAttribute ("colorName", key: "SECONDARY_CONTROL") {
|
|
||||||
attributeState "colorName", label:'${currentValue}'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||||
@@ -61,12 +60,12 @@ metadata {
|
|||||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") {
|
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") {
|
||||||
state "colorTemperature", action:"color temperature.setColorTemperature"
|
state "colorTemperature", action:"color temperature.setColorTemperature"
|
||||||
}
|
}
|
||||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||||
state "colorTemperature", label: '${currentValue} K'
|
state "colorName", label: '${currentValue}'
|
||||||
}
|
}
|
||||||
|
|
||||||
main(["switch"])
|
main(["switch"])
|
||||||
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
|
details(["switch", "colorTempSliderControl", "colorName", "refresh"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +74,13 @@ def parse(String description) {
|
|||||||
log.debug "description is $description"
|
log.debug "description is $description"
|
||||||
def event = zigbee.getEvent(description)
|
def event = zigbee.getEvent(description)
|
||||||
if (event) {
|
if (event) {
|
||||||
sendEvent(event)
|
if (event.name=="level" && event.value==0) {}
|
||||||
|
else {
|
||||||
|
if (event.name=="colorTemperature") {
|
||||||
|
setGenericName(event.value)
|
||||||
|
}
|
||||||
|
sendEvent(event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||||
@@ -95,13 +100,23 @@ def setLevel(value) {
|
|||||||
zigbee.setLevel(value)
|
zigbee.setLevel(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
|
* */
|
||||||
|
def ping() {
|
||||||
|
return zigbee.onOffRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig()
|
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
log.debug "Configuring Reporting and Bindings."
|
log.debug "Configuring Reporting and Bindings."
|
||||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
|
// Device-Watch allows 3 check-in misses from device. 300 seconds x 3 = 15min
|
||||||
|
sendEvent(name: "checkInterval", value: 900, displayed: false, data: [protocol: "zigbee"])
|
||||||
|
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||||
|
zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
def setColorTemperature(value) {
|
def setColorTemperature(value) {
|
||||||
|
|||||||
@@ -24,17 +24,24 @@ definition(
|
|||||||
iconX3Url: "http://www.gidjit.com/appicon@3x.png",
|
iconX3Url: "http://www.gidjit.com/appicon@3x.png",
|
||||||
oauth: [displayName: "Gidjit", displayLink: "www.gidjit.com"])
|
oauth: [displayName: "Gidjit", displayLink: "www.gidjit.com"])
|
||||||
|
|
||||||
|
preferences(oauthPage: "deviceAuthorization") {
|
||||||
|
// deviceAuthorization page is simply the devices to authorize
|
||||||
|
page(name: "deviceAuthorization", title: "Device Authorization", nextPage: "instructionPage",
|
||||||
|
install: false, uninstall: true) {
|
||||||
|
section ("Allow Gidjit to have access, thereby allowing you to quickly control and monitor your following devices. Privacy Policy can be found at http://priv.gidjit.com/privacy.html") {
|
||||||
|
input "switches", "capability.switch", title: "Control/Monitor your switches", multiple: true, required: false
|
||||||
|
input "thermostats", "capability.thermostat", title: "Control/Monitor your thermostats", multiple: true, required: false
|
||||||
|
input "windowShades", "capability.windowShade", title: "Control/Monitor your window shades", multiple: true, required: false //windowShade
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
preferences {
|
page(name: "instructionPage", title: "Device Discovery", install: true) {
|
||||||
section ("Allow Gidjit to have access, there by allowing you to quickly control and monitor the following devices") {
|
section() {
|
||||||
input "switches", "capability.switch", title: "Control/Monitor your switches", multiple: true, required: false
|
paragraph "Now the process is complete return to the Devices section of the Detected Screen. From there and you can add actions to each of your device panels, including launching SmartThings routines."
|
||||||
input "thermostats", "capability.thermostat", title: "Control/Monitor your thermostats", multiple: true, required: false
|
}
|
||||||
input "windowShades", "capability.windowShade", title: "Control/Monitor your window shades", multiple: true, required: false //windowShade
|
}
|
||||||
//input "bulbs", "capability.colorControl", title: "Control your lights", multiple: true, required: false //windowShade
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mappings {
|
mappings {
|
||||||
path("/structureinfo") {
|
path("/structureinfo") {
|
||||||
action: [
|
action: [
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ def humidityHandler(evt) {
|
|||||||
log.debug "Notification already sent within the last ${deltaMinutes} minutes"
|
log.debug "Notification already sent within the last ${deltaMinutes} minutes"
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
log.debug "Humidity Rose Above ${tooHumid}: sending SMS to $phone1 and activating ${mySwitch}"
|
log.debug "Humidity Rose Above ${tooHumid}: sending SMS and activating ${mySwitch}"
|
||||||
send("${humiditySensor1.label} sensed high humidity level of ${evt.value}")
|
send("${humiditySensor1.label} sensed high humidity level of ${evt.value}")
|
||||||
switch1?.on()
|
switch1?.on()
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ def humidityHandler(evt) {
|
|||||||
log.debug "Notification already sent within the last ${deltaMinutes} minutes"
|
log.debug "Notification already sent within the last ${deltaMinutes} minutes"
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
log.debug "Humidity Fell Below ${notHumidEnough}: sending SMS to $phone1 and activating ${mySwitch}"
|
log.debug "Humidity Fell Below ${notHumidEnough}: sending SMS and activating ${mySwitch}"
|
||||||
send("${humiditySensor1.label} sensed high humidity level of ${evt.value}")
|
send("${humiditySensor1.label} sensed high humidity level of ${evt.value}")
|
||||||
switch1?.off()
|
switch1?.off()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,15 +25,11 @@ preferences {
|
|||||||
|
|
||||||
def installed() {
|
def installed() {
|
||||||
subscribe(contact1, "contact", contactHandler)
|
subscribe(contact1, "contact", contactHandler)
|
||||||
subscribe(switch1, "switch.on", switchOnHandler)
|
|
||||||
subscribe(switch1, "switch.off", switchOffHandler)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def updated() {
|
def updated() {
|
||||||
unsubscribe()
|
unsubscribe()
|
||||||
subscribe(contact1, "contact", contactHandler)
|
subscribe(contact1, "contact", contactHandler)
|
||||||
subscribe(switch1, "switch.on", switchOnHandler)
|
|
||||||
subscribe(switch1, "switch.off", switchOffHandler)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def contactHandler(evt) {
|
def contactHandler(evt) {
|
||||||
@@ -46,4 +42,4 @@ def contactHandler(evt) {
|
|||||||
if (evt.value == "closed") {
|
if (evt.value == "closed") {
|
||||||
if(state.wasOn)switch1.on()
|
if(state.wasOn)switch1.on()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ def initialize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def sendit(evt) {
|
def sendit(evt) {
|
||||||
log.debug "$evt.value: $evt, $settings"
|
log.debug "$evt.value: $evt"
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
/**
|
||||||
|
* Medicine Management - Contact Sensor
|
||||||
|
*
|
||||||
|
* Copyright 2016 Jim Mangione
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* Logic:
|
||||||
|
* --- Send notification at the medicine reminder time IF draw wasn't alread opened in past 60 minutes
|
||||||
|
* --- If draw still isn't open 10 minutes AFTER reminder time, LED will turn RED.
|
||||||
|
* --- ----- Once draw IS open, LED will return back to it's original color
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
import groovy.time.TimeCategory
|
||||||
|
|
||||||
|
definition(
|
||||||
|
name: "Medicine Management - Contact Sensor",
|
||||||
|
namespace: "MangioneImagery",
|
||||||
|
author: "Jim Mangione",
|
||||||
|
description: "This supports devices with capabilities of ContactSensor and ColorControl (LED). It sends an in-app and ambient light notification if you forget to open the drawer or cabinet where meds are stored. A reminder will be set to a single time per day. If the draw or cabinet isn't opened within 60 minutes of that reminder, an in-app message will be sent. If the draw or cabinet still isn't opened after an additional 10 minutes, then an LED light turns red until the draw or cabinet is opened",
|
||||||
|
category: "Health & Wellness",
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
|
||||||
|
section("My Medicine Draw/Cabinet"){
|
||||||
|
input "deviceContactSensor", "capability.contactSensor", title: "Opened Sensor"
|
||||||
|
}
|
||||||
|
|
||||||
|
section("Remind me to take my medicine at"){
|
||||||
|
input "reminderTime", "time", title: "Time"
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Use REAL device - virtual device causes compilation errors
|
||||||
|
section("My LED Light"){
|
||||||
|
input "deviceLight", "capability.colorControl", title: "Smart light"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def installed() {
|
||||||
|
log.debug "Installed with settings: ${settings}"
|
||||||
|
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
def updated() {
|
||||||
|
log.debug "Updated with settings: ${settings}"
|
||||||
|
|
||||||
|
unsubscribe()
|
||||||
|
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
def initialize() {
|
||||||
|
|
||||||
|
// will stop LED notification incase it was set by med reminder
|
||||||
|
subscribe(deviceContactSensor, "contact", contactHandler)
|
||||||
|
|
||||||
|
// how many minutes to look in the past from the reminder time, for an open draw
|
||||||
|
state.minutesToCheckOpenDraw = 60
|
||||||
|
|
||||||
|
// is true when LED notification is set after exceeding 10 minutes past reminder time
|
||||||
|
state.ledNotificationTriggered = false
|
||||||
|
|
||||||
|
// Set a timer to run once a day to notify if draw wasn't opened yet
|
||||||
|
schedule(reminderTime, checkOpenDrawInPast)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should turn off any LED notification on OPEN state
|
||||||
|
def contactHandler(evt){
|
||||||
|
if (evt.value == "open") {
|
||||||
|
// if LED notification triggered, reset it.
|
||||||
|
log.debug "Cabinet opened"
|
||||||
|
if (state.ledNotificationTriggered) {
|
||||||
|
resetLEDNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the draw was NOT opened within 60 minutes of the timer send notification out.
|
||||||
|
def checkOpenDrawInPast(){
|
||||||
|
log.debug "Checking past 60 minutes of activity from $reminderTime"
|
||||||
|
|
||||||
|
// check activity of sensor for past 60 minutes for any OPENED status
|
||||||
|
def cabinetOpened = isOpened(state.minutesToCheckOpenDraw)
|
||||||
|
log.debug "Cabinet found opened: $cabinetOpened"
|
||||||
|
|
||||||
|
// if it's opened, then do nothing and assume they took their meds
|
||||||
|
if (!cabinetOpened) {
|
||||||
|
sendNotification("Hi, please remember to take your meds in the cabinet")
|
||||||
|
|
||||||
|
// if no open activity, send out notification and set new reminder
|
||||||
|
def reminderTimePlus10 = new Date(now() + (10 * 60000))
|
||||||
|
|
||||||
|
// needs to be scheduled if draw wasn't already opened
|
||||||
|
runOnce(reminderTimePlus10, checkOpenDrawAfterReminder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the draw was NOT opened after 10 minutes past reminder, use LED notification
|
||||||
|
def checkOpenDrawAfterReminder(){
|
||||||
|
log.debug "Checking additional 10 minutes of activity from $reminderTime"
|
||||||
|
|
||||||
|
// check activity of sensor for past 10 minutes for any OPENED status
|
||||||
|
def cabinetOpened = isOpened(10)
|
||||||
|
|
||||||
|
log.debug "Cabinet found opened: $cabinetOpened"
|
||||||
|
|
||||||
|
// if no open activity, blink lights
|
||||||
|
if (!cabinetOpened) {
|
||||||
|
log.debug "Set LED to Notification color"
|
||||||
|
setLEDNotification()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function for sending out an app notification
|
||||||
|
def sendNotification(msg){
|
||||||
|
log.debug "Message Sent: $msg"
|
||||||
|
sendPush(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the sensor has been opened since the minutes entered
|
||||||
|
// Return true if opened found, else false.
|
||||||
|
def isOpened(minutes){
|
||||||
|
// query last X minutes of activity log
|
||||||
|
def previousDateTime = new Date(now() - (minutes * 60000))
|
||||||
|
|
||||||
|
// capture all events recorded
|
||||||
|
def evts = deviceContactSensor.eventsSince(previousDateTime)
|
||||||
|
def cabinetOpened = false
|
||||||
|
if (evts.size() > 0) {
|
||||||
|
evts.each{
|
||||||
|
if(it.value == "open") {
|
||||||
|
cabinetOpened = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cabinetOpened
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saves current color and sets the light to RED
|
||||||
|
def setLEDNotification(){
|
||||||
|
|
||||||
|
state.ledNotificationTriggered = true
|
||||||
|
|
||||||
|
// turn light back off when reset is called if it was originally off
|
||||||
|
state.ledState = deviceLight.currentValue("switch")
|
||||||
|
|
||||||
|
// set light to RED and store original color until stopped
|
||||||
|
state.origColor = deviceLight.currentValue("hue")
|
||||||
|
deviceLight.on()
|
||||||
|
deviceLight.setHue(100)
|
||||||
|
|
||||||
|
log.debug "LED set to RED. Original color stored: $state.origColor"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the color back to the original saved color
|
||||||
|
def resetLEDNotification(){
|
||||||
|
|
||||||
|
state.ledNotificationTriggered = false
|
||||||
|
|
||||||
|
// return color to original
|
||||||
|
log.debug "Reset LED color to: $state.origColor"
|
||||||
|
if (state.origColor != null) {
|
||||||
|
deviceLight.setHue(state.origColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the light was turned on just for the notification, turn it back off now
|
||||||
|
if (state.ledState == "off") {
|
||||||
|
deviceLight.off()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
/**
|
||||||
|
* Medicine Management - Temp-Motion
|
||||||
|
*
|
||||||
|
* Copyright 2016 Jim Mangione
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* Logic:
|
||||||
|
* --- If temp > threshold set, send notification
|
||||||
|
* --- Send in-app notification at the medicine reminder time if no motion is detected in past 60 minutes
|
||||||
|
* --- If motion still isn't detected 10 minutes AFTER reminder time, LED will turn RED
|
||||||
|
* --- ----- Once motion is detected, LED will turn back to it's original color
|
||||||
|
*/
|
||||||
|
import groovy.time.TimeCategory
|
||||||
|
|
||||||
|
definition(
|
||||||
|
name: "Medicine Management - Temp-Motion",
|
||||||
|
namespace: "MangioneImagery",
|
||||||
|
author: "Jim Mangione",
|
||||||
|
description: "This only supports devices with capabilities TemperatureMeasurement, AccelerationSensor and ColorControl (LED). Supports two use cases. First, will notifies via in-app if the fridge where meds are stored exceeds a temperature threshold set in degrees. Secondly, sends an in-app and ambient light notification if you forget to take your meds by sensing movement of the medicine box in the fridge. A reminder will be set to a single time per day. If the box isn't moved within 60 minutes of that reminder, an in-app message will be sent. If the box still isn't moved after an additional 10 minutes, then an LED light turns red until the box is moved",
|
||||||
|
category: "Health & Wellness",
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
|
||||||
|
section("My Medicine in the Refrigerator"){
|
||||||
|
input "deviceAccelerationSensor", "capability.accelerationSensor", required: true, multiple: false, title: "Movement"
|
||||||
|
input "deviceTemperatureMeasurement", "capability.temperatureMeasurement", required: true, multiple: false, title: "Temperature"
|
||||||
|
}
|
||||||
|
|
||||||
|
section("Temperature Threshold"){
|
||||||
|
input "tempThreshold", "number", title: "Temperature Threshold"
|
||||||
|
}
|
||||||
|
|
||||||
|
section("Remind me to take my medicine at"){
|
||||||
|
input "reminderTime", "time", title: "Time"
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Use REAL device - virtual device causes compilation errors
|
||||||
|
section("My LED Light"){
|
||||||
|
input "deviceLight", "capability.colorControl", title: "Smart light"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
def installed() {
|
||||||
|
log.debug "Installed with settings: ${settings}"
|
||||||
|
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
def updated() {
|
||||||
|
log.debug "Updated with settings: ${settings}"
|
||||||
|
|
||||||
|
unsubscribe()
|
||||||
|
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
def initialize() {
|
||||||
|
// will notify when temp exceeds max
|
||||||
|
subscribe(deviceTemperatureMeasurement, "temperature", tempHandler)
|
||||||
|
|
||||||
|
// will stop LED notification incase it was set by med reminder
|
||||||
|
subscribe(deviceAccelerationSensor, "acceleration.active", motionHandler)
|
||||||
|
|
||||||
|
// how many minutes to look in the past from the reminder time
|
||||||
|
state.minutesToCheckPriorToReminder = 60
|
||||||
|
|
||||||
|
// Set a timer to run once a day to notify if draw wasn't opened yet
|
||||||
|
schedule(reminderTime, checkMotionInPast)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// If temp > 39 then send an app notification out.
|
||||||
|
def tempHandler(evt){
|
||||||
|
if (evt.doubleValue > tempThreshold) {
|
||||||
|
log.debug "Fridge temp of $evt.value exceeded threshold"
|
||||||
|
sendNotification("WARNING: Fridge temp is $evt.value with threshold of $tempThreshold")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should turn off any LED notification once motion detected
|
||||||
|
def motionHandler(evt){
|
||||||
|
// always call out to stop any possible LED notification
|
||||||
|
log.debug "Medication moved. Send stop LED notification"
|
||||||
|
resetLEDNotification()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no motion detected within 60 minutes of the timer send notification out.
|
||||||
|
def checkMotionInPast(){
|
||||||
|
log.debug "Checking past 60 minutes of activity from $reminderTime"
|
||||||
|
|
||||||
|
// check activity of sensor for past 60 minutes for any OPENED status
|
||||||
|
def movement = isMoved(state.minutesToCheckPriorToReminder)
|
||||||
|
log.debug "Motion found: $movement"
|
||||||
|
|
||||||
|
// if there was movement, then do nothing and assume they took their meds
|
||||||
|
if (!movement) {
|
||||||
|
sendNotification("Hi, please remember to take your meds in the fridge")
|
||||||
|
|
||||||
|
// if no movement, send out notification and set new reminder
|
||||||
|
def reminderTimePlus10 = new Date(now() + (10 * 60000))
|
||||||
|
|
||||||
|
// needs to be scheduled if draw wasn't already opened
|
||||||
|
runOnce(reminderTimePlus10, checkMotionAfterReminder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still no movement after 10 minutes past reminder, use LED notification
|
||||||
|
def checkMotionAfterReminder(){
|
||||||
|
log.debug "Checking additional 10 minutes of activity from $reminderTime"
|
||||||
|
|
||||||
|
// check activity of sensor for past 10 minutes for any OPENED status
|
||||||
|
def movement = isMoved(10)
|
||||||
|
|
||||||
|
log.debug "Motion found: $movement"
|
||||||
|
|
||||||
|
// if no open activity, blink lights
|
||||||
|
if (!movement) {
|
||||||
|
log.debug "Notify LED API"
|
||||||
|
setLEDNotification()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function for sending out an app notification
|
||||||
|
def sendNotification(msg){
|
||||||
|
log.debug "Message Sent: $msg"
|
||||||
|
sendPush(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the accelerometer has been activated since the minutes entered
|
||||||
|
// Return true if active, else false.
|
||||||
|
def isMoved(minutes){
|
||||||
|
// query last X minutes of activity log
|
||||||
|
def previousDateTime = new Date(now() - (minutes * 60000))
|
||||||
|
|
||||||
|
// capture all events recorded
|
||||||
|
def evts = deviceAccelerationSensor.eventsSince(previousDateTime)
|
||||||
|
def motion = false
|
||||||
|
if (evts.size() > 0) {
|
||||||
|
evts.each{
|
||||||
|
if(it.value == "active") {
|
||||||
|
motion = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return motion
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saves current color and sets the light to RED
|
||||||
|
def setLEDNotification(){
|
||||||
|
|
||||||
|
// turn light back off when reset is called if it was originally off
|
||||||
|
state.ledState = deviceLight.currentValue("switch")
|
||||||
|
|
||||||
|
// set light to RED and store original color until stopped
|
||||||
|
state.origColor = deviceLight.currentValue("hue")
|
||||||
|
deviceLight.on()
|
||||||
|
deviceLight.setHue(100)
|
||||||
|
|
||||||
|
log.debug "LED set to RED. Original color stored: $state.origColor"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the color back to the original saved color
|
||||||
|
def resetLEDNotification(){
|
||||||
|
|
||||||
|
// return color to original
|
||||||
|
log.debug "Reset LED color to: $state.origColor"
|
||||||
|
deviceLight.setHue(state.origColor)
|
||||||
|
|
||||||
|
// if the light was turned on just for the notification, turn it back off now
|
||||||
|
if (state.ledState == "off") {
|
||||||
|
deviceLight.off()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ definition(
|
|||||||
name: "Monitor on Sense",
|
name: "Monitor on Sense",
|
||||||
namespace: "resteele",
|
namespace: "resteele",
|
||||||
author: "Rachel Steele",
|
author: "Rachel Steele",
|
||||||
description: "Turn on Monitor when vibration is sensed",
|
description: "Turn on switch when vibration is sensed",
|
||||||
category: "My Apps",
|
category: "My Apps",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
||||||
@@ -25,10 +25,10 @@ definition(
|
|||||||
|
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
section("When the keyboard is used...") {
|
section("When vibration is sensed...") {
|
||||||
input "accelerationSensor", "capability.accelerationSensor", title: "Which Sensor?"
|
input "accelerationSensor", "capability.accelerationSensor", title: "Which Sensor?"
|
||||||
}
|
}
|
||||||
section("Turn on/off a light...") {
|
section("Turn on switch...") {
|
||||||
input "switch1", "capability.switch"
|
input "switch1", "capability.switch"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,5 +47,3 @@ def updated() {
|
|||||||
def accelerationActiveHandler(evt) {
|
def accelerationActiveHandler(evt) {
|
||||||
switch1.on()
|
switch1.on()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ def humidityHandler(evt) {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (state.lastStatus != "off") {
|
if (state.lastStatus != "off") {
|
||||||
log.debug "Humidity Rose Above $humidityHigh1: sending SMS to $phone1 and deactivating $mySwitch"
|
log.debug "Humidity Rose Above $humidityHigh1: sending SMS and deactivating $mySwitch"
|
||||||
send("${humiditySensor1.label} sensed high humidity level of ${evt.value}, turning off ${switch1.label}")
|
send("${humiditySensor1.label} sensed high humidity level of ${evt.value}, turning off ${switch1.label}")
|
||||||
switch1?.off()
|
switch1?.off()
|
||||||
state.lastStatus = "off"
|
state.lastStatus = "off"
|
||||||
@@ -99,7 +99,7 @@ def humidityHandler(evt) {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (state.lastStatus != "on") {
|
if (state.lastStatus != "on") {
|
||||||
log.debug "Humidity Dropped Below $humidityLow1: sending SMS to $phone1 and activating $mySwitch"
|
log.debug "Humidity Dropped Below $humidityLow1: sending SMS and activating $mySwitch"
|
||||||
send("${humiditySensor1.label} sensed low humidity level of ${evt.value}, turning on ${switch1.label}")
|
send("${humiditySensor1.label} sensed low humidity level of ${evt.value}, turning on ${switch1.label}")
|
||||||
switch1?.on()
|
switch1?.on()
|
||||||
state.lastStatus = "on"
|
state.lastStatus = "on"
|
||||||
@@ -125,4 +125,4 @@ private send(msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.debug msg
|
log.debug 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,13 +115,12 @@ 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)}"
|
||||||
@@ -129,9 +128,6 @@ 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) {
|
||||||
@@ -148,8 +144,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)
|
||||||
}
|
}
|
||||||
@@ -171,64 +167,63 @@ 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%;
|
||||||
/*background: #eee;*/
|
text-align: center;
|
||||||
text-align: center;
|
}
|
||||||
}
|
img {
|
||||||
img {
|
vertical-align: middle;
|
||||||
vertical-align: middle;
|
}
|
||||||
}
|
p {
|
||||||
p {
|
font-size: 2.2em;
|
||||||
font-size: 2.2em;
|
font-family: 'Swiss 721 W01 Thin';
|
||||||
font-family: 'Swiss 721 W01 Thin';
|
text-align: center;
|
||||||
text-align: center;
|
color: #666666;
|
||||||
color: #666666;
|
padding: 0 40px;
|
||||||
padding: 0 40px;
|
margin-bottom: 0;
|
||||||
margin-bottom: 0;
|
}
|
||||||
}
|
span {
|
||||||
span {
|
font-family: 'Swiss 721 W01 Light';
|
||||||
font-family: 'Swiss 721 W01 Light';
|
}
|
||||||
}
|
</style>
|
||||||
</style>
|
</head>
|
||||||
</head>
|
<body>
|
||||||
<body>
|
<div class="container">
|
||||||
<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
|
||||||
}
|
}
|
||||||
@@ -237,19 +232,26 @@ def getEcobeeThermostats() {
|
|||||||
log.debug "getting device list"
|
log.debug "getting device list"
|
||||||
atomicState.remoteSensors = []
|
atomicState.remoteSensors = []
|
||||||
|
|
||||||
def requestBody = '{"selection":{"selectionType":"registered","selectionMatch":"","includeRuntime":true,"includeSensors":true}}'
|
def bodyParams = [
|
||||||
|
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}"],
|
||||||
query: [format: 'json', body: requestBody]
|
// TODO - the query string below is not consistent with the Ecobee docs:
|
||||||
|
// https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml
|
||||||
|
query: [format: 'json', body: toJson(bodyParams)]
|
||||||
]
|
]
|
||||||
|
|
||||||
def stats = [:]
|
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
|
||||||
@@ -289,9 +291,10 @@ 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) {
|
||||||
@@ -310,7 +313,6 @@ 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)
|
||||||
@@ -350,8 +352,6 @@ 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,75 +381,41 @@ 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}"],
|
||||||
query: [format: 'json', body: jsonRequestBody]
|
// TODO - the query string below is not consistent with the Ecobee docs:
|
||||||
]
|
// https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml
|
||||||
|
query: [format: 'json', body: toJson(requestBody)]
|
||||||
|
]
|
||||||
|
|
||||||
try{
|
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
|
||||||
atomicState.thermostatData = resp.data
|
updateSensorData()
|
||||||
updateSensorData()
|
storeThermostatData(resp.data.thermostatList)
|
||||||
atomicState.thermostats = resp.data.thermostatList.inject([:]) { collector, stat ->
|
result = true
|
||||||
def dni = [ app.id, stat.identifier ].join('.')
|
log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}"
|
||||||
|
}
|
||||||
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
|
||||||
@@ -463,13 +429,12 @@ 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}"
|
||||||
@@ -492,36 +457,7 @@ void poll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def availableModes(child) {
|
def availableModes(child) {
|
||||||
|
|
||||||
debugEvent ("atomicState.thermostats = ${atomicState.thermostats}")
|
debugEvent ("atomicState.thermostats = ${atomicState.thermostats}")
|
||||||
|
|
||||||
debugEvent ("Child DNI = ${child.device.deviceNetworkId}")
|
|
||||||
|
|
||||||
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
|
||||||
|
|
||||||
debugEvent("Data = ${tData}")
|
|
||||||
|
|
||||||
if(!tData)
|
|
||||||
{
|
|
||||||
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling"
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
def modes = ["off"]
|
|
||||||
|
|
||||||
if (tData.data.heatMode) modes.add("heat")
|
|
||||||
if (tData.data.coolMode) modes.add("cool")
|
|
||||||
if (tData.data.autoMode) modes.add("auto")
|
|
||||||
if (tData.data.auxHeatMode) modes.add("auxHeatOnly")
|
|
||||||
|
|
||||||
modes
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
def currentMode(child) {
|
|
||||||
debugEvent ("atomicState.Thermos = ${atomicState.thermostats}")
|
|
||||||
|
|
||||||
debugEvent ("Child DNI = ${child.device.deviceNetworkId}")
|
debugEvent ("Child DNI = ${child.device.deviceNetworkId}")
|
||||||
|
|
||||||
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
||||||
@@ -530,14 +466,42 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
|
def modes = ["off"]
|
||||||
|
|
||||||
|
if (tData.data.heatMode) {
|
||||||
|
modes.add("heat")
|
||||||
|
}
|
||||||
|
if (tData.data.coolMode) {
|
||||||
|
modes.add("cool")
|
||||||
|
}
|
||||||
|
if (tData.data.autoMode) {
|
||||||
|
modes.add("auto")
|
||||||
|
}
|
||||||
|
if (tData.data.auxHeatMode) {
|
||||||
|
modes.add("auxHeatOnly")
|
||||||
|
}
|
||||||
|
|
||||||
|
return modes
|
||||||
|
}
|
||||||
|
|
||||||
|
def currentMode(child) {
|
||||||
|
debugEvent ("atomicState.Thermos = ${atomicState.thermostats}")
|
||||||
|
debugEvent ("Child DNI = ${child.device.deviceNetworkId}")
|
||||||
|
|
||||||
|
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
||||||
|
|
||||||
|
debugEvent("Data = ${tData}")
|
||||||
|
|
||||||
|
if(!tData) {
|
||||||
|
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling"
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
def mode = tData.data.thermostatMode
|
def mode = tData.data.thermostatMode
|
||||||
|
return mode
|
||||||
mode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def updateSensorData() {
|
def updateSensorData() {
|
||||||
@@ -558,12 +522,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
|
||||||
@@ -582,7 +546,7 @@ def getChildDeviceIdsString() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def toJson(Map m) {
|
def toJson(Map m) {
|
||||||
return new org.json.JSONObject(m).toString()
|
return groovy.json.JsonOutput.toJson(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
def toQueryString(Map m) {
|
def toQueryString(Map m) {
|
||||||
@@ -595,54 +559,24 @@ 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
|
||||||
@@ -662,118 +596,220 @@ 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
|
||||||
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}]}'
|
* an expired token.
|
||||||
def result = sendJson(jsonRequestBody)
|
*
|
||||||
return result
|
* @param json - an object representing the parsed JSON response from Ecobee
|
||||||
|
*/
|
||||||
|
private void saveTokenAndResumeAction(json) {
|
||||||
|
log.debug "token response json: $json"
|
||||||
|
if (json) {
|
||||||
|
debugEvent("Response = $json")
|
||||||
|
atomicState.refreshToken = json?.refresh_token
|
||||||
|
atomicState.authToken = json?.access_token
|
||||||
|
if (atomicState.action) {
|
||||||
|
log.debug "got refresh token, executing next action: ${atomicState.action}"
|
||||||
|
"${atomicState.action}"()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn "did not get response body from refresh token response"
|
||||||
|
}
|
||||||
|
atomicState.action = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
def setHold(child, heating, cooling, deviceId, sendHoldType) {
|
/**
|
||||||
|
* Executes the resume program command on the Ecobee thermostat
|
||||||
int h = heating * 10
|
* @param deviceId - the ID of the device
|
||||||
int c = cooling * 10
|
*
|
||||||
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": '+sendHoldType+' } } ]}'
|
* @retrun true if the command was successful, false otherwise.
|
||||||
|
*/
|
||||||
def result = sendJson(child, jsonRequestBody)
|
boolean resumeProgram(deviceId) {
|
||||||
return result
|
def payload = [
|
||||||
|
selection: [
|
||||||
|
selectionType: "thermostats",
|
||||||
|
selectionMatch: deviceId,
|
||||||
|
includeRuntime: true
|
||||||
|
],
|
||||||
|
functions: [
|
||||||
|
[
|
||||||
|
type: "resumeProgram"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
return sendCommandToEcobee(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
def setFanMode(child, heating, cooling, deviceId, sendHoldType, fanMode) {
|
/**
|
||||||
|
* Executes the set hold command on the Ecobee thermostat
|
||||||
|
* @param heating - The heating temperature to set in fahrenheit
|
||||||
|
* @param cooling - the cooling temperature to set in fahrenheit
|
||||||
|
* @param deviceId - the ID of the device
|
||||||
|
* @param sendHoldType - the hold type to execute
|
||||||
|
*
|
||||||
|
* @return true if the command was successful, false otherwise
|
||||||
|
*/
|
||||||
|
boolean setHold(heating, cooling, deviceId, sendHoldType) {
|
||||||
|
// Ecobee requires that temp values be in fahrenheit multiplied by 10.
|
||||||
|
int h = heating * 10
|
||||||
|
int c = cooling * 10
|
||||||
|
|
||||||
int h = heating * 10
|
def payload = [
|
||||||
int c = cooling * 10
|
selection: [
|
||||||
|
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) {
|
/**
|
||||||
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"thermostat": {"settings":{"hvacMode":"'+"${mode}"+'"}}}'
|
* Executes the set fan mode command on the Ecobee thermostat
|
||||||
|
* @param heating - The heating temperature to set in fahrenheit
|
||||||
|
* @param cooling - the cooling temperature to set in fahrenheit
|
||||||
|
* @param deviceId - the ID of the device
|
||||||
|
* @param sendHoldType - the hold type to execute
|
||||||
|
* @param fanMode - the fan mode to set to
|
||||||
|
*
|
||||||
|
* @return true if the command was successful, false otherwise
|
||||||
|
*/
|
||||||
|
boolean setFanMode(heating, cooling, deviceId, sendHoldType, fanMode) {
|
||||||
|
// Ecobee requires that temp values be in fahrenheit multiplied by 10.
|
||||||
|
int h = heating * 10
|
||||||
|
int c = cooling * 10
|
||||||
|
|
||||||
def result = sendJson(jsonRequestBody)
|
def payload = [
|
||||||
return result
|
selection: [
|
||||||
|
selectionType: "thermostats",
|
||||||
|
selectionMatch: deviceId,
|
||||||
|
includeRuntime: true
|
||||||
|
],
|
||||||
|
functions: [
|
||||||
|
[
|
||||||
|
type: "setHold",
|
||||||
|
params: [
|
||||||
|
coolHoldTemp: c,
|
||||||
|
heatHoldTemp: h,
|
||||||
|
holdType: sendHoldType,
|
||||||
|
fan: fanMode
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
return sendCommandToEcobee(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
def sendJson(child = null, String jsonBody) {
|
/**
|
||||||
|
* Sets the mode of the Ecobee thermostat
|
||||||
|
* @param mode - the mode to set to
|
||||||
|
* @param deviceId - the ID of the device
|
||||||
|
*
|
||||||
|
* @return true if the command was successful, false otherwise
|
||||||
|
*/
|
||||||
|
boolean setMode(mode, deviceId) {
|
||||||
|
def payload = [
|
||||||
|
selection: [
|
||||||
|
selectionType: "thermostats",
|
||||||
|
selectionMatch: deviceId,
|
||||||
|
includeRuntime: true
|
||||||
|
],
|
||||||
|
thermostat: [
|
||||||
|
settings: [
|
||||||
|
hvacMode: mode
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
return sendCommandToEcobee(payload)
|
||||||
|
}
|
||||||
|
|
||||||
def returnStatus = false
|
/**
|
||||||
|
* Makes a request to the Ecobee API to actuate the thermostat.
|
||||||
|
* Used by command methods to send commands to Ecobee.
|
||||||
|
*
|
||||||
|
* @param bodyParams - a map of request parameters to send to Ecobee.
|
||||||
|
*
|
||||||
|
* @return true if the command was accepted by Ecobee without error, false otherwise.
|
||||||
|
*/
|
||||||
|
private boolean sendCommandToEcobee(Map bodyParams) {
|
||||||
|
def isSuccess = false
|
||||||
def cmdParams = [
|
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: jsonBody
|
body: toJson(bodyParams)
|
||||||
]
|
]
|
||||||
|
|
||||||
try{
|
try{
|
||||||
httpPost(cmdParams) { resp ->
|
httpPost(cmdParams) { resp ->
|
||||||
|
if(resp.status == 200) {
|
||||||
if(resp.status == 200) {
|
log.debug "updated ${resp.data}"
|
||||||
|
def returnStatus = resp.data.status.code
|
||||||
log.debug "updated ${resp.data}"
|
if (returnStatus == 0) {
|
||||||
returnStatus = resp.data.status.code
|
log.debug "Successful call to ecobee API."
|
||||||
if (resp.data.status.code == 0)
|
isSuccess = true
|
||||||
log.debug "Successful call to ecobee API."
|
} else {
|
||||||
else {
|
log.debug "Error return code = ${returnStatus}"
|
||||||
log.debug "Error return code = ${resp.data.status.code}"
|
debugEvent("Error return code = ${returnStatus}")
|
||||||
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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (returnStatus == 0)
|
return isSuccess
|
||||||
return true
|
|
||||||
else
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def getChildName() { "Ecobee Thermostat" }
|
def getChildName() { return "Ecobee Thermostat" }
|
||||||
def getSensorChildName() { "Ecobee Sensor" }
|
def getSensorChildName() { return "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() { "https://graph.api.smartthings.com/oauth/callback" }
|
def getCallbackUrl() { return "https://graph.api.smartthings.com/oauth/callback" }
|
||||||
def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${shardUrl}" }
|
def getBuildRedirectUrl() { return "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${shardUrl}" }
|
||||||
def getApiEndpoint() { "https://api.ecobee.com" }
|
def getApiEndpoint() { return "https://api.ecobee.com" }
|
||||||
def getSmartThingsClientId() { appSettings.clientId }
|
def getSmartThingsClientId() { return 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()
|
||||||
@@ -786,6 +822,58 @@ 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 ->
|
||||||
@@ -793,14 +881,6 @@ 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()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -64,10 +64,12 @@ def meterHandler(evt) {
|
|||||||
def lastValue = atomicState.lastValue as double
|
def lastValue = atomicState.lastValue as double
|
||||||
atomicState.lastValue = meterValue
|
atomicState.lastValue = meterValue
|
||||||
|
|
||||||
|
def dUnit = evt.unit ?: "Watts"
|
||||||
|
|
||||||
def aboveThresholdValue = aboveThreshold as int
|
def aboveThresholdValue = aboveThreshold as int
|
||||||
if (meterValue > aboveThresholdValue) {
|
if (meterValue > aboveThresholdValue) {
|
||||||
if (lastValue < aboveThresholdValue) { // only send notifications when crossing the threshold
|
if (lastValue < aboveThresholdValue) { // only send notifications when crossing the threshold
|
||||||
def msg = "${meter} reported ${evt.value} ${evt.unit} which is above your threshold of ${aboveThreshold}."
|
def msg = "${meter} reported ${evt.value} ${dUnit} which is above your threshold of ${aboveThreshold}."
|
||||||
sendMessage(msg)
|
sendMessage(msg)
|
||||||
} else {
|
} else {
|
||||||
// log.debug "not sending notification for ${evt.description} because the threshold (${aboveThreshold}) has already been crossed"
|
// log.debug "not sending notification for ${evt.description} because the threshold (${aboveThreshold}) has already been crossed"
|
||||||
@@ -78,7 +80,7 @@ def meterHandler(evt) {
|
|||||||
def belowThresholdValue = belowThreshold as int
|
def belowThresholdValue = belowThreshold as int
|
||||||
if (meterValue < belowThresholdValue) {
|
if (meterValue < belowThresholdValue) {
|
||||||
if (lastValue > belowThresholdValue) { // only send notifications when crossing the threshold
|
if (lastValue > belowThresholdValue) { // only send notifications when crossing the threshold
|
||||||
def msg = "${meter} reported ${evt.value} ${evt.unit} which is below your threshold of ${belowThreshold}."
|
def msg = "${meter} reported ${evt.value} ${dUnit} which is below your threshold of ${belowThreshold}."
|
||||||
sendMessage(msg)
|
sendMessage(msg)
|
||||||
} else {
|
} else {
|
||||||
// log.debug "not sending notification for ${evt.description} because the threshold (${belowThreshold}) has already been crossed"
|
// log.debug "not sending notification for ${evt.description} because the threshold (${belowThreshold}) has already been crossed"
|
||||||
|
|||||||
@@ -761,7 +761,7 @@ String displayableTime(timeRemaining) {
|
|||||||
return "${minutes}:00"
|
return "${minutes}:00"
|
||||||
}
|
}
|
||||||
def fraction = "0.${parts[1]}" as double
|
def fraction = "0.${parts[1]}" as double
|
||||||
def seconds = "${60 * fraction as int}".padRight(2, "0")
|
def seconds = "${60 * fraction as int}".padLeft(2, "0")
|
||||||
return "${minutes}:${seconds}"
|
return "${minutes}:${seconds}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1101,4 +1101,4 @@ def hasStartLevel() {
|
|||||||
|
|
||||||
def hasEndLevel() {
|
def hasEndLevel() {
|
||||||
return (endLevel != null && endLevel != "")
|
return (endLevel != null && endLevel != "")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ def scheduleCheck()
|
|||||||
sendNotificationToContacts("No one has fed the dog", recipients)
|
sendNotificationToContacts("No one has fed the dog", recipients)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log.debug "Feeder was not opened since $midnight, texting $phone1"
|
log.debug "Feeder was not opened since $midnight, texting one phone number"
|
||||||
sendSms(phone1, "No one has fed the dog")
|
sendSms(phone1, "No one has fed the dog")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -69,10 +69,10 @@ def temperatureHandler(evt) {
|
|||||||
def alreadySentSms = recentEvents.count { it.doubleValue <= tooCold } > 1
|
def alreadySentSms = recentEvents.count { it.doubleValue <= tooCold } > 1
|
||||||
|
|
||||||
if (alreadySentSms) {
|
if (alreadySentSms) {
|
||||||
log.debug "SMS already sent to $phone1 within the last $deltaMinutes minutes"
|
log.debug "SMS already sent within the last $deltaMinutes minutes"
|
||||||
// TODO: Send "Temperature back to normal" SMS, turn switch off
|
// TODO: Send "Temperature back to normal" SMS, turn switch off
|
||||||
} else {
|
} else {
|
||||||
log.debug "Temperature dropped below $tooCold: sending SMS to $phone1 and activating $mySwitch"
|
log.debug "Temperature dropped below $tooCold: sending SMS and activating $mySwitch"
|
||||||
def tempScale = location.temperatureScale ?: "F"
|
def tempScale = location.temperatureScale ?: "F"
|
||||||
send("${temperatureSensor1.displayName} is too cold, reporting a temperature of ${evt.value}${evt.unit?:tempScale}")
|
send("${temperatureSensor1.displayName} is too cold, reporting a temperature of ${evt.value}${evt.unit?:tempScale}")
|
||||||
switch1?.on()
|
switch1?.on()
|
||||||
|
|||||||
@@ -74,8 +74,6 @@ 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
|
||||||
@@ -257,8 +255,6 @@ 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
|
||||||
@@ -269,8 +265,6 @@ 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=="
|
||||||
@@ -284,8 +278,6 @@ def initializeLife360Connection() {
|
|||||||
"username=${username}&"+
|
"username=${username}&"+
|
||||||
"password=${password}"
|
"password=${password}"
|
||||||
|
|
||||||
log.debug "Post Body: ${postBody}"
|
|
||||||
|
|
||||||
def result = null
|
def result = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -295,7 +287,6 @@ 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}"
|
||||||
@@ -533,8 +524,6 @@ def createCircleSubscription() {
|
|||||||
|
|
||||||
def postBody = "url=${hookUrl}"
|
def postBody = "url=${hookUrl}"
|
||||||
|
|
||||||
log.debug "Post Body: ${postBody}"
|
|
||||||
|
|
||||||
def result = null
|
def result = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -586,8 +575,6 @@ 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: "Phone Number (for SMS, optional)", required: false
|
input "phone", "phone", title: "Enter a phone number to get SMS", required: false
|
||||||
paragraph "If outside the US please make sure to enter the proper country code"
|
paragraph "If outside the US please make sure to enter the proper country code"
|
||||||
input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes", "No"]
|
input "pushAndPhone", "enum", title: "Notify me via Push Notification", required: false, options: ["Yes", "No"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
section("Minimum time between messages (optional, defaults to every message)") {
|
section("Minimum time between messages (optional, defaults to every message)") {
|
||||||
@@ -111,19 +111,24 @@ private sendMessage(evt) {
|
|||||||
if (location.contactBookEnabled) {
|
if (location.contactBookEnabled) {
|
||||||
sendNotificationToContacts(msg, recipients, options)
|
sendNotificationToContacts(msg, recipients, options)
|
||||||
} else {
|
} else {
|
||||||
if (!phone || pushAndPhone != 'No') {
|
|
||||||
log.debug 'sending push'
|
|
||||||
options.method = 'push'
|
|
||||||
//sendPush(msg)
|
|
||||||
}
|
|
||||||
if (phone) {
|
if (phone) {
|
||||||
options.phone = phone
|
options.phone = phone
|
||||||
log.debug 'sending SMS'
|
if (pushAndPhone != 'No') {
|
||||||
//sendSms(phone, msg)
|
log.debug 'Sending push and SMS'
|
||||||
|
options.method = 'both'
|
||||||
|
} else {
|
||||||
|
log.debug 'Sending SMS'
|
||||||
|
options.method = 'phone'
|
||||||
|
}
|
||||||
|
} else if (pushAndPhone != 'No') {
|
||||||
|
log.debug 'Sending push'
|
||||||
|
options.method = 'push'
|
||||||
|
} else {
|
||||||
|
log.debug 'Sending nothing'
|
||||||
|
options.method = 'none'
|
||||||
}
|
}
|
||||||
sendNotification(msg, options)
|
sendNotification(msg, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frequency) {
|
if (frequency) {
|
||||||
state[evt.deviceId] = now()
|
state[evt.deviceId] = now()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user