mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-14 21:03:19 +00:00
Compare commits
202 Commits
PROD_2016.
...
netatmo-ap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dd201be3b | ||
|
|
9433598381 | ||
|
|
723ef7e7e6 | ||
|
|
84c72de640 | ||
|
|
570454e6c3 | ||
|
|
a5d95fb025 | ||
|
|
50696902cf | ||
|
|
409658e899 | ||
|
|
1068a553f5 | ||
|
|
bbdf9ff02a | ||
|
|
f5b7dfd4eb | ||
|
|
3e0306e912 | ||
|
|
2c2d75ae37 | ||
|
|
61ef40831c | ||
|
|
19169748df | ||
|
|
0f5a2c5e21 | ||
|
|
6dbb61536b | ||
|
|
84323afa04 | ||
|
|
04a7627c21 | ||
|
|
12b09acfa8 | ||
|
|
9e8ad0dfdf | ||
|
|
80eb1e43b9 | ||
|
|
af383de368 | ||
|
|
427fa88ed8 | ||
|
|
57514944d5 | ||
|
|
823efed562 | ||
|
|
540db429f3 | ||
|
|
0c3a5de661 | ||
|
|
989f08708b | ||
|
|
60e09c56b7 | ||
|
|
62b37f5c3d | ||
|
|
64e4ccc517 | ||
|
|
c17830ab56 | ||
|
|
aa890ae3d5 | ||
|
|
8d701b9fea | ||
|
|
c7f78a69e4 | ||
|
|
80500207a8 | ||
|
|
29db335e1c | ||
|
|
55b5b7d03d | ||
|
|
730ccccd45 | ||
|
|
719b24ecd6 | ||
|
|
9d5ab3bfc8 | ||
|
|
218cc43520 | ||
|
|
5b0ca4b815 | ||
|
|
9ddc020f04 | ||
|
|
aab3b8d7f8 | ||
|
|
a0ccf35eaa | ||
|
|
9fbbaec8f6 | ||
|
|
e4c1824afd | ||
|
|
797a58cb68 | ||
|
|
c428267d63 | ||
|
|
02f30cf425 | ||
|
|
fea802ffce | ||
|
|
6400d26f4a | ||
|
|
5e3aaa3270 | ||
|
|
f5c3997679 | ||
|
|
81cf1179ef | ||
|
|
7113d7470e | ||
|
|
79d20b0edb | ||
|
|
b6d862fdd4 | ||
|
|
d58084c438 | ||
|
|
dbfaef3e69 | ||
|
|
40ed88e7fd | ||
|
|
1d6e22dc16 | ||
|
|
30993aa218 | ||
|
|
2f8ed277ff | ||
|
|
1d180ac487 | ||
|
|
230541a145 | ||
|
|
8c4f7edc83 | ||
|
|
4f188581df | ||
|
|
71880e2644 | ||
|
|
0b7bb40474 | ||
|
|
8d920ea072 | ||
|
|
e373b6f92e | ||
|
|
43a1ae6371 | ||
|
|
60a98e3074 | ||
|
|
a441b94a33 | ||
|
|
ced03d746d | ||
|
|
5341d0d06f | ||
|
|
2a58d7ff62 | ||
|
|
260917d515 | ||
|
|
c1478d3e96 | ||
|
|
8b9bff15dc | ||
|
|
75c1ede16c | ||
|
|
a7acc384a2 | ||
|
|
c6998e5f1d | ||
|
|
f95e906d6e | ||
|
|
a6c7ab49b6 | ||
|
|
4891e3b947 | ||
|
|
ae91f9bff5 | ||
|
|
bb87ad2cf0 | ||
|
|
5dff03fb69 | ||
|
|
629d768575 | ||
|
|
dd7c6b90d5 | ||
|
|
4523498dab | ||
|
|
e89e45e000 | ||
|
|
78ec280e83 | ||
|
|
1f144d36e4 | ||
|
|
f5bd580c9e | ||
|
|
d5329dbde3 | ||
|
|
48818cfb06 | ||
|
|
079919260b | ||
|
|
570922e2ac | ||
|
|
53ed9b4d2b | ||
|
|
7149a81c85 | ||
|
|
30274f0cd7 | ||
|
|
8869cd3af0 | ||
|
|
fb0caa6446 | ||
|
|
3d05d42cb8 | ||
|
|
3184615e87 | ||
|
|
0f70362e0a | ||
|
|
bc817f8530 | ||
|
|
01b8399893 | ||
|
|
81318bafac | ||
|
|
60c2006bfc | ||
|
|
1e4f1223e7 | ||
|
|
b78bce55b2 | ||
|
|
01593c3973 | ||
|
|
6862785d6c | ||
|
|
763d7411e2 | ||
|
|
c703543f36 | ||
|
|
5db6ecda3e | ||
|
|
43b836f413 | ||
|
|
006b5e7bea | ||
|
|
62c8c19805 | ||
|
|
48e9a4bd6a | ||
|
|
07a4c0decc | ||
|
|
6aa09bb052 | ||
|
|
2f889de11a | ||
|
|
5584020e96 | ||
|
|
4ef2e694c2 | ||
|
|
826993cc45 | ||
|
|
ae3306928b | ||
|
|
8777ec5f6d | ||
|
|
91eb59a10d | ||
|
|
324ac13afb | ||
|
|
878eb66b8b | ||
|
|
8dfc270c2d | ||
|
|
614573a15c | ||
|
|
9c7b0875ba | ||
|
|
7568cbf781 | ||
|
|
1858c280a5 | ||
|
|
6b7d0968f6 | ||
|
|
159d3acf4f | ||
|
|
e8101630a3 | ||
|
|
f5ba78b221 | ||
|
|
19b8a7eeb9 | ||
|
|
1b37d649a5 | ||
|
|
dedb0f8465 | ||
|
|
06acc13575 | ||
|
|
c051d719cc | ||
|
|
1e54b93b0c | ||
|
|
bac37f9ca2 | ||
|
|
f0ecb65c09 | ||
|
|
1c0ddd2571 | ||
|
|
b7e0cbda09 | ||
|
|
f80e094bd9 | ||
|
|
ce9ac624d0 | ||
|
|
f3f5cc42c9 | ||
|
|
313fe8b734 | ||
|
|
0d693386d1 | ||
|
|
d1aee1e874 | ||
|
|
3528e7da51 | ||
|
|
fe2fbc3b97 | ||
|
|
5c2e06c98d | ||
|
|
26df619b4f | ||
|
|
af2ea04442 | ||
|
|
383f72580a | ||
|
|
090a306939 | ||
|
|
d0a16c10b2 | ||
|
|
faa65f204d | ||
|
|
bacd335991 | ||
|
|
740e5e096c | ||
|
|
aac2f9b177 | ||
|
|
048eb77e64 | ||
|
|
dadec937fa | ||
|
|
78aa6691c4 | ||
|
|
315918dc6f | ||
|
|
5e6b4f74e0 | ||
|
|
276f7d3b43 | ||
|
|
ce12ad5013 | ||
|
|
beed783d19 | ||
|
|
7f347638d5 | ||
|
|
1f8ce734e7 | ||
|
|
17bf040c7e | ||
|
|
f1c3f5942b | ||
|
|
212c9c4179 | ||
|
|
4898006e4e | ||
|
|
d3eb7f756f | ||
|
|
b95ba37364 | ||
|
|
87f8755faf | ||
|
|
555a9f5ab4 | ||
|
|
0744384dbf | ||
|
|
97e0e9d0f8 | ||
|
|
655e756b1b | ||
|
|
7ce7ad86bd | ||
|
|
24c64608a9 | ||
|
|
dbc2a1e45c | ||
|
|
92cc8afdf7 | ||
|
|
e545842f7c | ||
|
|
4ad0a6fd9d | ||
|
|
22185c5440 |
30
build.gradle
30
build.gradle
@@ -19,7 +19,7 @@ buildscript {
|
|||||||
username smartThingsArtifactoryUserName
|
username smartThingsArtifactoryUserName
|
||||||
password smartThingsArtifactoryPassword
|
password smartThingsArtifactoryPassword
|
||||||
}
|
}
|
||||||
url "http://artifactory.smartthings.com/libs-release-local"
|
url "https://artifactory.smartthings.com/libs-release-local"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,9 +27,37 @@ buildscript {
|
|||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
jcenter()
|
jcenter()
|
||||||
|
maven {
|
||||||
|
credentials {
|
||||||
|
username smartThingsArtifactoryUserName
|
||||||
|
password smartThingsArtifactoryPassword
|
||||||
|
}
|
||||||
|
url "https://artifactory.smartthings.com/libs-release-local"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
devicetypes {
|
||||||
|
groovy {
|
||||||
|
srcDirs = ['devicetypes']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
smartapps {
|
||||||
|
groovy {
|
||||||
|
srcDirs = ['smartapps']
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
devicetypesCompile 'org.codehaus.groovy:groovy-all:2.4.7'
|
||||||
|
devicetypesCompile 'smartthings:appengine-z-wave:0.1.2'
|
||||||
|
devicetypesCompile 'smartthings:appengine-zigbee:0.1.11'
|
||||||
|
smartappsCompile 'org.codehaus.groovy:groovy-all:2.4.7'
|
||||||
|
smartappsCompile 'smartthings:appengine-common:0.1.8'
|
||||||
|
smartappsCompile 'org.codehaus.groovy.modules.http-builder:http-builder:0.7.1'
|
||||||
|
smartappsCompile 'org.grails:grails-web:2.3.11'
|
||||||
|
smartappsCompile 'org.json:json:20140107'
|
||||||
}
|
}
|
||||||
|
|
||||||
slackSendMessage {
|
slackSendMessage {
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ machine:
|
|||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
override:
|
override:
|
||||||
- echo "Nothing to do."
|
- ./gradlew dependencies -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD"
|
||||||
|
post:
|
||||||
|
- ./gradlew compileSmartappsGroovy compileDevicetypesGroovy -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD"
|
||||||
|
|
||||||
test:
|
test:
|
||||||
override:
|
override:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "Netatmo Additional Module", namespace: "dianoga", author: "Brian Steere") {
|
definition (name: "Netatmo Additional Module", namespace: "dianoga", author: "Brian Steere") {
|
||||||
|
capability "Sensor"
|
||||||
capability "Relative Humidity Measurement"
|
capability "Relative Humidity Measurement"
|
||||||
capability "Temperature Measurement"
|
capability "Temperature Measurement"
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "Netatmo Basestation", namespace: "dianoga", author: "Brian Steere") {
|
definition (name: "Netatmo Basestation", namespace: "dianoga", author: "Brian Steere") {
|
||||||
|
capability "Sensor"
|
||||||
capability "Relative Humidity Measurement"
|
capability "Relative Humidity Measurement"
|
||||||
capability "Temperature Measurement"
|
capability "Temperature Measurement"
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "Netatmo Outdoor Module", namespace: "dianoga", author: "Brian Steere") {
|
definition (name: "Netatmo Outdoor Module", namespace: "dianoga", author: "Brian Steere") {
|
||||||
|
capability "Sensor"
|
||||||
capability "Relative Humidity Measurement"
|
capability "Relative Humidity Measurement"
|
||||||
capability "Temperature Measurement"
|
capability "Temperature Measurement"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "Netatmo Rain", namespace: "dianoga", author: "Brian Steere") {
|
definition (name: "Netatmo Rain", namespace: "dianoga", author: "Brian Steere") {
|
||||||
|
capability "Sensor"
|
||||||
|
|
||||||
attribute "rain", "number"
|
attribute "rain", "number"
|
||||||
attribute "rainSumHour", "number"
|
attribute "rainSumHour", "number"
|
||||||
attribute "rainSumDay", "number"
|
attribute "rainSumDay", "number"
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ metadata {
|
|||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
multiAttributeTile(name:"FGMS", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app
|
multiAttributeTile(name:"FGMS", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app
|
||||||
tileAttribute("device.motion", key:"PRIMARY_CONTROL") {
|
tileAttribute("device.motion", key:"PRIMARY_CONTROL") {
|
||||||
attributeState("inactive", icon:"st.motion.motion.inactive", backgroundColor:"#79b821")
|
attributeState("inactive", label:"no motion", icon:"st.motion.motion.inactive", backgroundColor:"#79b821")
|
||||||
attributeState("active", icon:"st.motion.motion.active", backgroundColor:"#ffa81e")
|
attributeState("active", label:"motion", icon:"st.motion.motion.active", backgroundColor:"#ffa81e")
|
||||||
}
|
}
|
||||||
|
|
||||||
tileAttribute("device.tamper", key:"SECONDARY_CONTROL") {
|
tileAttribute("device.tamper", key:"SECONDARY_CONTROL") {
|
||||||
@@ -127,9 +127,10 @@ def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelR
|
|||||||
def map = [ displayed: true ]
|
def map = [ displayed: true ]
|
||||||
switch (cmd.sensorType) {
|
switch (cmd.sensorType) {
|
||||||
case 1:
|
case 1:
|
||||||
map.name = "temperature"
|
def cmdScale = cmd.scale == 1 ? "F" : "C"
|
||||||
map.unit = cmd.scale == 1 ? "F" : "C"
|
map.name = "temperature"
|
||||||
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, map.unit, cmd.precision)
|
map.unit = getTemperatureScale()
|
||||||
|
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision)
|
||||||
break
|
break
|
||||||
case 3:
|
case 3:
|
||||||
map.name = "illuminance"
|
map.name = "illuminance"
|
||||||
@@ -278,4 +279,4 @@ private encap(physicalgraph.zwave.Command cmd) {
|
|||||||
} else {
|
} else {
|
||||||
crc16(cmd)
|
crc16(cmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,16 +87,27 @@ def beep() {
|
|||||||
up to this long from the time you send the message to the time you hear a sound.
|
up to this long from the time you send the message to the time you hear a sound.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Used source endpoint of 0x02 because we are using smartthings manufacturer specific cluster.
|
||||||
[
|
[
|
||||||
"raw 0xFC05 {15 0A 11 00 00 15 01}",
|
"raw 0xFC05 {15 0A 11 00 00 15 01}",
|
||||||
|
"delay 200",
|
||||||
|
"send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId",
|
||||||
"delay 7000",
|
"delay 7000",
|
||||||
"raw 0xFC05 {15 0A 11 00 00 15 01}",
|
"raw 0xFC05 {15 0A 11 00 00 15 01}",
|
||||||
|
"delay 200",
|
||||||
|
"send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId",
|
||||||
"delay 7000",
|
"delay 7000",
|
||||||
"raw 0xFC05 {15 0A 11 00 00 15 01}",
|
"raw 0xFC05 {15 0A 11 00 00 15 01}",
|
||||||
|
"delay 200",
|
||||||
|
"send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId",
|
||||||
"delay 7000",
|
"delay 7000",
|
||||||
"raw 0xFC05 {15 0A 11 00 00 15 01}",
|
"raw 0xFC05 {15 0A 11 00 00 15 01}",
|
||||||
|
"delay 200",
|
||||||
|
"send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId",
|
||||||
"delay 7000",
|
"delay 7000",
|
||||||
"raw 0xFC05 {15 0A 11 00 00 15 01}"
|
"raw 0xFC05 {15 0A 11 00 00 15 01}",
|
||||||
|
"delay 200",
|
||||||
|
"send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -105,11 +105,21 @@ def parseDescriptionAsMap(description) {
|
|||||||
|
|
||||||
// Commands to device
|
// Commands to device
|
||||||
def on() {
|
def on() {
|
||||||
'zcl on-off on'
|
[
|
||||||
|
'zcl on-off on',
|
||||||
|
'delay 200',
|
||||||
|
"send 0x${zigbee.deviceNetworkId} 0x01 0x${zigbee.endpointId}",
|
||||||
|
'delay 500'
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
def off() {
|
def off() {
|
||||||
'zcl on-off off'
|
[
|
||||||
|
'zcl on-off off',
|
||||||
|
'delay 200',
|
||||||
|
"send 0x${zigbee.deviceNetworkId} 0x01 0x${zigbee.endpointId}",
|
||||||
|
'delay 500'
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
def setLevel(value) {
|
def setLevel(value) {
|
||||||
|
|||||||
@@ -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
|
||||||
34
devicetypes/smartthings/cree-bulb.src/README.md
Normal file
34
devicetypes/smartthings/cree-bulb.src/README.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# 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 5 mins.
|
||||||
|
Check-in interval = 12 mins
|
||||||
|
|
||||||
|
## 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)
|
||||||
@@ -19,7 +19,6 @@ metadata {
|
|||||||
|
|
||||||
capability "Actuator"
|
capability "Actuator"
|
||||||
capability "Configuration"
|
capability "Configuration"
|
||||||
capability "Polling"
|
|
||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
capability "Switch"
|
capability "Switch"
|
||||||
capability "Switch Level"
|
capability "Switch Level"
|
||||||
@@ -67,12 +66,6 @@ def parse(String description) {
|
|||||||
def resultMap = zigbee.getEvent(description)
|
def resultMap = zigbee.getEvent(description)
|
||||||
if (resultMap) {
|
if (resultMap) {
|
||||||
sendEvent(resultMap)
|
sendEvent(resultMap)
|
||||||
// Temporary fix for the case when Device is OFFLINE and is connected again
|
|
||||||
if (state.lastActivity == null){
|
|
||||||
state.lastActivity = now()
|
|
||||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
|
||||||
}
|
|
||||||
state.lastActivity = now()
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log.debug "DID NOT PARSE MESSAGE for description : $description"
|
log.debug "DID NOT PARSE MESSAGE for description : $description"
|
||||||
@@ -96,27 +89,23 @@ def setLevel(value) {
|
|||||||
* PING is used by Device-Watch in attempt to reach the Device
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
* */
|
* */
|
||||||
def ping() {
|
def ping() {
|
||||||
|
return zigbee.levelRefresh()
|
||||||
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
|
|
||||||
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
|
|
||||||
state.lastActivity = null
|
|
||||||
return zigbee.levelRefresh()
|
|
||||||
} else {
|
|
||||||
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
|
|
||||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
def poll() {
|
|
||||||
zigbee.onOffRefresh() + zigbee.levelRefresh()
|
zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def healthPoll() {
|
||||||
log.debug "Configuring Reporting and Bindings."
|
log.debug "healthPoll()"
|
||||||
sendEvent(name: "checkInterval", value: 1200, displayed: false, data: [protocol: "zigbee"])
|
def cmds = zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
|
cmds.each{ sendHubCommand(new physicalgraph.device.HubAction(it))}
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
unschedule()
|
||||||
|
runEvery5Minutes("healthPoll")
|
||||||
|
// Device-Watch allows 2 check-in misses from device
|
||||||
|
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
|
zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,13 +31,13 @@ metadata {
|
|||||||
command "switchMode"
|
command "switchMode"
|
||||||
command "switchFanMode"
|
command "switchFanMode"
|
||||||
|
|
||||||
attribute "thermostatSetpoint","number"
|
attribute "thermostatSetpoint", "number"
|
||||||
attribute "thermostatStatus","string"
|
attribute "thermostatStatus", "string"
|
||||||
attribute "maxHeatingSetpoint", "number"
|
attribute "maxHeatingSetpoint", "number"
|
||||||
attribute "minHeatingSetpoint", "number"
|
attribute "minHeatingSetpoint", "number"
|
||||||
attribute "maxCoolingSetpoint", "number"
|
attribute "maxCoolingSetpoint", "number"
|
||||||
attribute "minCoolingSetpoint", "number"
|
attribute "minCoolingSetpoint", "number"
|
||||||
attribute "deviceTemperatureUnit", "number"
|
attribute "deviceTemperatureUnit", "string"
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles {
|
tiles {
|
||||||
@@ -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
|
||||||
@@ -655,55 +655,60 @@ void lowerSetpoint() {
|
|||||||
void alterSetpoint(temp) {
|
void alterSetpoint(temp) {
|
||||||
|
|
||||||
def mode = device.currentValue("thermostatMode")
|
def mode = device.currentValue("thermostatMode")
|
||||||
def heatingSetpoint = device.currentValue("heatingSetpoint")
|
|
||||||
def coolingSetpoint = device.currentValue("coolingSetpoint")
|
|
||||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
|
||||||
|
|
||||||
def targetHeatingSetpoint
|
if (mode == "off" || mode == "auto") {
|
||||||
def targetCoolingSetpoint
|
log.warn "this mode: $mode does not allow alterSetpoint"
|
||||||
|
|
||||||
//step1: check thermostatMode, enforce limits before sending request to cloud
|
|
||||||
if (mode == "heat" || mode == "auxHeatOnly"){
|
|
||||||
if (temp.value > coolingSetpoint){
|
|
||||||
targetHeatingSetpoint = temp.value
|
|
||||||
targetCoolingSetpoint = temp.value
|
|
||||||
} else {
|
|
||||||
targetHeatingSetpoint = temp.value
|
|
||||||
targetCoolingSetpoint = coolingSetpoint
|
|
||||||
}
|
|
||||||
} else if (mode == "cool") {
|
|
||||||
//enforce limits before sending request to cloud
|
|
||||||
if (temp.value < heatingSetpoint){
|
|
||||||
targetHeatingSetpoint = temp.value
|
|
||||||
targetCoolingSetpoint = temp.value
|
|
||||||
} else {
|
|
||||||
targetHeatingSetpoint = heatingSetpoint
|
|
||||||
targetCoolingSetpoint = temp.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug "alterSetpoint >> in mode ${mode} trying to change heatingSetpoint to $targetHeatingSetpoint " +
|
|
||||||
"coolingSetpoint to $targetCoolingSetpoint with holdType : ${holdType}"
|
|
||||||
|
|
||||||
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
|
|
||||||
|
|
||||||
def coolingValue = location.temperatureScale == "C"? convertCtoF(targetCoolingSetpoint) : targetCoolingSetpoint
|
|
||||||
def heatingValue = location.temperatureScale == "C"? convertCtoF(targetHeatingSetpoint) : targetHeatingSetpoint
|
|
||||||
|
|
||||||
if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) {
|
|
||||||
sendEvent("name": "thermostatSetpoint", "value": temp.value, displayed: false)
|
|
||||||
sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint)
|
|
||||||
sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint)
|
|
||||||
log.debug "alterSetpoint in mode $mode succeed change setpoint to= ${temp.value}"
|
|
||||||
} else {
|
} else {
|
||||||
log.error "Error alterSetpoint()"
|
def heatingSetpoint = device.currentValue("heatingSetpoint")
|
||||||
|
def coolingSetpoint = device.currentValue("coolingSetpoint")
|
||||||
|
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||||
|
|
||||||
|
def targetHeatingSetpoint
|
||||||
|
def targetCoolingSetpoint
|
||||||
|
|
||||||
|
//step1: check thermostatMode, enforce limits before sending request to cloud
|
||||||
if (mode == "heat" || mode == "auxHeatOnly"){
|
if (mode == "heat" || mode == "auxHeatOnly"){
|
||||||
sendEvent("name": "thermostatSetpoint", "value": heatingSetpoint.toString(), displayed: false)
|
if (temp.value > coolingSetpoint){
|
||||||
|
targetHeatingSetpoint = temp.value
|
||||||
|
targetCoolingSetpoint = temp.value
|
||||||
|
} else {
|
||||||
|
targetHeatingSetpoint = temp.value
|
||||||
|
targetCoolingSetpoint = coolingSetpoint
|
||||||
|
}
|
||||||
} else if (mode == "cool") {
|
} else if (mode == "cool") {
|
||||||
sendEvent("name": "thermostatSetpoint", "value": coolingSetpoint.toString(), displayed: false)
|
//enforce limits before sending request to cloud
|
||||||
|
if (temp.value < heatingSetpoint){
|
||||||
|
targetHeatingSetpoint = temp.value
|
||||||
|
targetCoolingSetpoint = temp.value
|
||||||
|
} else {
|
||||||
|
targetHeatingSetpoint = heatingSetpoint
|
||||||
|
targetCoolingSetpoint = temp.value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.debug "alterSetpoint >> in mode ${mode} trying to change heatingSetpoint to $targetHeatingSetpoint " +
|
||||||
|
"coolingSetpoint to $targetCoolingSetpoint with holdType : ${holdType}"
|
||||||
|
|
||||||
|
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
|
||||||
|
|
||||||
|
def coolingValue = location.temperatureScale == "C"? convertCtoF(targetCoolingSetpoint) : targetCoolingSetpoint
|
||||||
|
def heatingValue = location.temperatureScale == "C"? convertCtoF(targetHeatingSetpoint) : targetHeatingSetpoint
|
||||||
|
|
||||||
|
if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) {
|
||||||
|
sendEvent("name": "thermostatSetpoint", "value": temp.value, displayed: false)
|
||||||
|
sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint, "unit": location.temperatureScale)
|
||||||
|
sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint, "unit": location.temperatureScale)
|
||||||
|
log.debug "alterSetpoint in mode $mode succeed change setpoint to= ${temp.value}"
|
||||||
|
} else {
|
||||||
|
log.error "Error alterSetpoint()"
|
||||||
|
if (mode == "heat" || mode == "auxHeatOnly"){
|
||||||
|
sendEvent("name": "thermostatSetpoint", "value": heatingSetpoint.toString(), displayed: false)
|
||||||
|
} else if (mode == "cool") {
|
||||||
|
sendEvent("name": "thermostatSetpoint", "value": coolingSetpoint.toString(), displayed: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
generateStatusEvent()
|
||||||
}
|
}
|
||||||
generateStatusEvent()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def generateStatusEvent() {
|
def generateStatusEvent() {
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -55,6 +56,10 @@ metadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void installed() {
|
||||||
|
sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "lan", hubHardwareId: device.hub.hardwareID], displayed: false)
|
||||||
|
}
|
||||||
|
|
||||||
// parse events into attributes
|
// parse events into attributes
|
||||||
def parse(description) {
|
def parse(description) {
|
||||||
log.debug "parse() - $description"
|
log.debug "parse() - $description"
|
||||||
@@ -166,3 +171,7 @@ def verifyPercent(percent) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def ping() {
|
||||||
|
log.debug "${parent.ping(this)}"
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,9 +7,11 @@
|
|||||||
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") {
|
||||||
|
capability "Health Check"
|
||||||
|
|
||||||
attribute "networkAddress", "string"
|
attribute "networkAddress", "string"
|
||||||
// Used to indicate if bridge is reachable or not, i.e. is the bridge connected to the network
|
// Used to indicate if bridge is reachable or not, i.e. is the bridge connected to the network
|
||||||
// Possible values "Online" or "Offline"
|
// Possible values "Online" or "Offline"
|
||||||
attribute "status", "string"
|
attribute "status", "string"
|
||||||
// Id is the number on the back of the hub, Hue uses last six digits of Mac address
|
// 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
|
// This is also used in the Hue application as ID
|
||||||
@@ -42,6 +44,10 @@ metadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void installed() {
|
||||||
|
sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "lan"], displayed: false)
|
||||||
|
}
|
||||||
|
|
||||||
// parse events into attributes
|
// parse events into attributes
|
||||||
def parse(description) {
|
def parse(description) {
|
||||||
log.debug "Parsing '${description}'"
|
log.debug "Parsing '${description}'"
|
||||||
@@ -62,7 +68,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"]
|
||||||
@@ -70,18 +76,17 @@ def parse(description) {
|
|||||||
def bulbs = new groovy.json.JsonSlurper().parseText(msg.body)
|
def bulbs = new groovy.json.JsonSlurper().parseText(msg.body)
|
||||||
if (bulbs.state) {
|
if (bulbs.state) {
|
||||||
log.info "Bridge response: $msg.body"
|
log.info "Bridge response: $msg.body"
|
||||||
} else {
|
|
||||||
// Sending Bulbs List to parent"
|
|
||||||
if (parent.state.inBulbDiscovery)
|
|
||||||
log.info parent.bulbListHandler(device.hub.id, msg.body)
|
|
||||||
}
|
}
|
||||||
}
|
} 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results
|
results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def ping() {
|
||||||
|
log.debug "${parent.ping(this)}"
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -64,6 +65,10 @@ metadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void installed() {
|
||||||
|
sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "lan", hubHardwareId: device.hub.hardwareID], displayed: false)
|
||||||
|
}
|
||||||
|
|
||||||
// parse events into attributes
|
// parse events into attributes
|
||||||
def parse(description) {
|
def parse(description) {
|
||||||
log.debug "parse() - $description"
|
log.debug "parse() - $description"
|
||||||
@@ -169,7 +174,7 @@ void setColorTemperature(value) {
|
|||||||
|
|
||||||
void refresh() {
|
void refresh() {
|
||||||
log.debug "Executing 'refresh'"
|
log.debug "Executing 'refresh'"
|
||||||
parent.manualRefresh()
|
parent?.manualRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
def verifyPercent(percent) {
|
def verifyPercent(percent) {
|
||||||
@@ -182,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 * 12, data: [protocol: "lan", hubHardwareId: device.hub.hardwareID], displayed: false)
|
||||||
|
}
|
||||||
|
|
||||||
// parse events into attributes
|
// parse events into attributes
|
||||||
def parse(description) {
|
def parse(description) {
|
||||||
log.debug "parse() - $description"
|
log.debug "parse() - $description"
|
||||||
@@ -87,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"
|
||||||
}
|
}
|
||||||
@@ -53,6 +54,10 @@ metadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void installed() {
|
||||||
|
sendEvent(name: "checkInterval", value: 60 * 12, data: [protocol: "lan", hubHardwareId: device.hub.hardwareID], displayed: false)
|
||||||
|
}
|
||||||
|
|
||||||
// parse events into attributes
|
// parse events into attributes
|
||||||
def parse(description) {
|
def parse(description) {
|
||||||
log.debug "parse() - $description"
|
log.debug "parse() - $description"
|
||||||
@@ -101,3 +106,7 @@ void refresh() {
|
|||||||
log.debug "Executing 'refresh'"
|
log.debug "Executing 'refresh'"
|
||||||
parent.manualRefresh()
|
parent.manualRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def ping() {
|
||||||
|
log.debug "${parent.ping(this)}"
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.st-ignore
|
||||||
|
README.md
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Nyce Door/Window Sensor (Open/Close Sensor)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Works with:
|
||||||
|
|
||||||
|
* [NYCE Door/Window Sensor NCZ-3011](https://support.smartthings.com/hc/en-us/articles/204576764-NYCE-Door-Window-Sensor)
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
* [Capabilities](#capabilities)
|
||||||
|
* [Health](#device-health)
|
||||||
|
* [Battery](#battery-specification)
|
||||||
|
* [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
* **Configuration** - _configure()_ command called when device is installed or device preferences updated
|
||||||
|
* **Contact Sensor** - can detect contact (with possible values - open/closed)
|
||||||
|
* **Battery** - defines device uses a battery
|
||||||
|
* **Refresh** - _refresh()_ command for status updates
|
||||||
|
* **Health Check** - indicates ability to get device health notifications
|
||||||
|
|
||||||
|
## Device Health
|
||||||
|
|
||||||
|
A Category C2 Nyce Door/Window sensor that has 12min check-in interval
|
||||||
|
|
||||||
|
## Battery Specification
|
||||||
|
|
||||||
|
One 3V CR2032 battery 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 sensor from SmartThings can be found in the following link:
|
||||||
|
* [Nyce Door/Window Sensor](https://support.smartthings.com/hc/en-us/articles/204576764-NYCE-Door-Window-Sensor)
|
||||||
@@ -19,25 +19,26 @@ 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"
|
||||||
capability "Configuration"
|
capability "Configuration"
|
||||||
capability "Contact Sensor"
|
capability "Contact Sensor"
|
||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
|
capability "Health Check"
|
||||||
command "enrollResponse"
|
|
||||||
|
command "enrollResponse"
|
||||||
|
|
||||||
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3010", deviceJoinName: "NYCE Door Hinge Sensor"
|
|
||||||
|
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3010", deviceJoinName: "NYCE Door Hinge Sensor"
|
||||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor"
|
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor"
|
||||||
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor"
|
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor"
|
||||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor"
|
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor"
|
||||||
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor"
|
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor"
|
||||||
}
|
}
|
||||||
|
|
||||||
simulator {
|
simulator {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles {
|
tiles {
|
||||||
standardTile("contact", "device.contact", width: 2, height: 2) {
|
standardTile("contact", "device.contact", width: 2, height: 2) {
|
||||||
state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
|
state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
|
||||||
@@ -273,23 +274,28 @@ private List parseIasMessage(String description) {
|
|||||||
return resultListMap
|
return resultListMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
|
* */
|
||||||
|
def ping() {
|
||||||
|
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
||||||
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
|
// Device-Watch allows 2 check-in misses from device
|
||||||
|
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
|
|
||||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||||
|
|
||||||
def configCmds = [
|
def enrollCmds = [
|
||||||
//battery reporting and heartbeat
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200",
|
|
||||||
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", "delay 200",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500",
|
|
||||||
|
|
||||||
|
|
||||||
// Writes CIE attribute on end device to direct reports to the hub's EUID
|
// Writes CIE attribute on end device to direct reports to the hub's EUID
|
||||||
"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",
|
||||||
]
|
]
|
||||||
|
|
||||||
log.debug "configure: Write IAS CIE"
|
log.debug "configure: Write IAS CIE"
|
||||||
return configCmds
|
// battery minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||||
|
return enrollCmds + zigbee.batteryConfig(30, 300) + refresh() // send refresh cmds as part of config
|
||||||
}
|
}
|
||||||
|
|
||||||
def enrollResponse() {
|
def enrollResponse() {
|
||||||
@@ -334,7 +340,8 @@ Integer convertHexToInt(hex) {
|
|||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
log.debug "Refreshing Battery"
|
log.debug "Refreshing Battery"
|
||||||
[
|
def refreshCmds = [
|
||||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 1 0x20", "delay 200"
|
"st rattr 0x${device.deviceNetworkId} ${endpointId} 1 0x20", "delay 200"
|
||||||
]
|
]
|
||||||
|
return refreshCmds + enrollResponse()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ metadata {
|
|||||||
attribute "colorName", "string"
|
attribute "colorName", "string"
|
||||||
|
|
||||||
command "setAdjustedColor"
|
command "setAdjustedColor"
|
||||||
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenspot RGB"
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Gardenspot RGB"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// simulator metadata
|
// simulator metadata
|
||||||
@@ -91,7 +88,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 +200,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()
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ def refresh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
onOffConfig() + levelConfig() + powerConfig() + refresh()
|
refresh() + onOffConfig() + levelConfig() + powerConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -47,9 +47,21 @@ def parse(String description) {
|
|||||||
|
|
||||||
// Commands to device
|
// Commands to device
|
||||||
def on() {
|
def on() {
|
||||||
'zcl on-off on'
|
[
|
||||||
|
'zcl on-off on',
|
||||||
|
'delay 200',
|
||||||
|
"send 0x${zigbee.deviceNetworkId} 0x01 0x${zigbee.endpointId}",
|
||||||
|
'delay 500'
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def off() {
|
def off() {
|
||||||
'zcl on-off off'
|
[
|
||||||
|
'zcl on-off off',
|
||||||
|
'delay 200',
|
||||||
|
"send 0x${zigbee.deviceNetworkId} 0x01 0x${zigbee.endpointId}",
|
||||||
|
'delay 500'
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
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 5 mins.
|
||||||
|
Check-in interval is double the value of maxReportTime.
|
||||||
|
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||||
|
Check-in interval = 12 mins
|
||||||
|
|
||||||
|
## 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)
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
// Automatically generated. Make future change here.
|
// Automatically generated. Make future change here.
|
||||||
definition (name: "SmartPower Outlet", namespace: "smartthings", author: "SmartThings", category: "C1") {
|
definition (name: "SmartPower Outlet", namespace: "smartthings", author: "SmartThings") {
|
||||||
capability "Actuator"
|
capability "Actuator"
|
||||||
capability "Switch"
|
capability "Switch"
|
||||||
capability "Power Meter"
|
capability "Power Meter"
|
||||||
@@ -101,17 +101,24 @@ def parse(String description) {
|
|||||||
else {
|
else {
|
||||||
def descriptionText = finalResult.value == "on" ? '{{ device.displayName }} is On' : '{{ device.displayName }} is Off'
|
def descriptionText = finalResult.value == "on" ? '{{ device.displayName }} is On' : '{{ device.displayName }} is Off'
|
||||||
sendEvent(name: finalResult.type, value: finalResult.value, descriptionText: descriptionText, translatable: true)
|
sendEvent(name: finalResult.type, value: finalResult.value, descriptionText: descriptionText, translatable: true)
|
||||||
// Temporary fix for the case when Device is OFFLINE and is connected again
|
|
||||||
if (state.lastActivity == null){
|
|
||||||
state.lastActivity = now()
|
|
||||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
|
||||||
}
|
|
||||||
state.lastActivity = now()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
def cluster = zigbee.parse(description)
|
||||||
log.debug zigbee.parseDescriptionAsMap(description)
|
|
||||||
|
if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07){
|
||||||
|
if (cluster.data[0] == 0x00) {
|
||||||
|
log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster
|
||||||
|
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||||
|
log.debug "${cluster}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,15 +133,7 @@ def on() {
|
|||||||
* PING is used by Device-Watch in attempt to reach the Device
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
* */
|
* */
|
||||||
def ping() {
|
def ping() {
|
||||||
|
return zigbee.onOffRefresh()
|
||||||
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
|
|
||||||
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
|
|
||||||
state.lastActivity = null
|
|
||||||
return zigbee.onOffRefresh()
|
|
||||||
} else {
|
|
||||||
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
|
|
||||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
@@ -142,8 +141,12 @@ def refresh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
sendEvent(name: "checkInterval", value: 1200, displayed: false, data: [protocol: "zigbee"])
|
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||||
zigbee.onOffConfig() + powerConfig() + refresh()
|
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||||
|
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
|
|
||||||
|
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||||
|
refresh() + zigbee.onOffConfig(0, 300) + powerConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
//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 5 mins.
|
||||||
|
Check-in interval is double the value of maxReportTime.
|
||||||
|
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||||
|
Check-in interval = 12 mins
|
||||||
|
|
||||||
|
## 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)
|
||||||
@@ -17,7 +17,7 @@ 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") {
|
||||||
capability "Configuration"
|
capability "Configuration"
|
||||||
capability "Battery"
|
capability "Battery"
|
||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
@@ -101,13 +101,6 @@ def parse(String description) {
|
|||||||
map = parseIasMessage(description)
|
map = parseIasMessage(description)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporary fix for the case when Device is OFFLINE and is connected again
|
|
||||||
if (state.lastActivity == null){
|
|
||||||
state.lastActivity = now()
|
|
||||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
|
||||||
}
|
|
||||||
state.lastActivity = now()
|
|
||||||
|
|
||||||
log.debug "Parse returned $map"
|
log.debug "Parse returned $map"
|
||||||
def result = map ? createEvent(map) : null
|
def result = map ? createEvent(map) : null
|
||||||
|
|
||||||
@@ -125,14 +118,28 @@ private Map parseCatchAllMessage(String description) {
|
|||||||
if (shouldProcessMessage(cluster)) {
|
if (shouldProcessMessage(cluster)) {
|
||||||
switch(cluster.clusterId) {
|
switch(cluster.clusterId) {
|
||||||
case 0x0001:
|
case 0x0001:
|
||||||
resultMap = getBatteryResult(cluster.data.last())
|
// 0x07 - configure reporting
|
||||||
|
if (cluster.command != 0x07) {
|
||||||
|
resultMap = getBatteryResult(cluster.data.last())
|
||||||
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 0x0402:
|
case 0x0402:
|
||||||
// temp is last 2 data values. reverse to swap endian
|
if (cluster.command == 0x07) {
|
||||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
if (cluster.data[0] == 0x00){
|
||||||
def value = getTemperature(temp)
|
log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster
|
||||||
resultMap = getTemperatureResult(value)
|
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// temp is last 2 data values. reverse to swap endian
|
||||||
|
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||||
|
def value = getTemperature(temp)
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,10 +149,8 @@ private Map parseCatchAllMessage(String description) {
|
|||||||
|
|
||||||
private boolean shouldProcessMessage(cluster) {
|
private boolean shouldProcessMessage(cluster) {
|
||||||
// 0x0B is default response indicating message got through
|
// 0x0B is default response indicating message got through
|
||||||
// 0x07 is bind message
|
|
||||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||||
cluster.command == 0x0B ||
|
cluster.command == 0x0B ||
|
||||||
cluster.command == 0x07 ||
|
|
||||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||||
return !ignoredMessage
|
return !ignoredMessage
|
||||||
}
|
}
|
||||||
@@ -187,9 +192,9 @@ private Map parseIasMessage(String description) {
|
|||||||
def getTemperature(value) {
|
def getTemperature(value) {
|
||||||
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
||||||
if(getTemperatureScale() == "C"){
|
if(getTemperatureScale() == "C"){
|
||||||
return celsius
|
return Math.round(celsius)
|
||||||
} else {
|
} else {
|
||||||
return celsiusToFahrenheit(celsius) as Integer
|
return Math.round(celsiusToFahrenheit(celsius))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,6 +238,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 }}%"
|
||||||
}
|
}
|
||||||
@@ -259,7 +266,8 @@ private Map getTemperatureResult(value) {
|
|||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText,
|
descriptionText: descriptionText,
|
||||||
translatable: true
|
translatable: true,
|
||||||
|
unit: temperatureScale
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,15 +290,7 @@ private Map getMoistureResult(value) {
|
|||||||
* PING is used by Device-Watch in attempt to reach the Device
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
* */
|
* */
|
||||||
def ping() {
|
def ping() {
|
||||||
|
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
||||||
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
|
|
||||||
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
|
|
||||||
state.lastActivity = null
|
|
||||||
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
|
||||||
} else {
|
|
||||||
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
|
|
||||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
@@ -304,23 +304,13 @@ def refresh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
sendEvent(name: "checkInterval", value: 14400, displayed: false, data: [protocol: "zigbee"])
|
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||||
|
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||||
|
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
|
|
||||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
// battery minReport 30 seconds, maxReportTime 6 hrs by default
|
||||||
def configCmds = [
|
return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config
|
||||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 5 mins.
|
||||||
|
Check-in interval is double the value of maxReportTime.
|
||||||
|
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||||
|
Check-in interval = 12 mins
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ 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") {
|
||||||
capability "Motion Sensor"
|
capability "Motion Sensor"
|
||||||
capability "Configuration"
|
capability "Configuration"
|
||||||
capability "Battery"
|
capability "Battery"
|
||||||
@@ -105,13 +105,6 @@ def parse(String description) {
|
|||||||
map = parseIasMessage(description)
|
map = parseIasMessage(description)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporary fix for the case when Device is OFFLINE and is connected again
|
|
||||||
if (state.lastActivity == null){
|
|
||||||
state.lastActivity = now()
|
|
||||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
|
||||||
}
|
|
||||||
state.lastActivity = now()
|
|
||||||
|
|
||||||
log.debug "Parse returned $map"
|
log.debug "Parse returned $map"
|
||||||
def result = map ? createEvent(map) : null
|
def result = map ? createEvent(map) : null
|
||||||
|
|
||||||
@@ -129,19 +122,37 @@ private Map parseCatchAllMessage(String description) {
|
|||||||
if (shouldProcessMessage(cluster)) {
|
if (shouldProcessMessage(cluster)) {
|
||||||
switch(cluster.clusterId) {
|
switch(cluster.clusterId) {
|
||||||
case 0x0001:
|
case 0x0001:
|
||||||
resultMap = getBatteryResult(cluster.data.last())
|
// 0x07 - configure reporting
|
||||||
|
if (cluster.command != 0x07) {
|
||||||
|
resultMap = getBatteryResult(cluster.data.last())
|
||||||
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 0x0402:
|
case 0x0402:
|
||||||
// temp is last 2 data values. reverse to swap endian
|
if (cluster.command == 0x07) {
|
||||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
if (cluster.data[0] == 0x00) {
|
||||||
def value = getTemperature(temp)
|
log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster
|
||||||
resultMap = getTemperatureResult(value)
|
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// temp is last 2 data values. reverse to swap endian
|
||||||
|
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||||
|
def value = getTemperature(temp)
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 0x0406:
|
case 0x0406:
|
||||||
log.debug 'motion'
|
// 0x07 - configure reporting
|
||||||
resultMap.name = 'motion'
|
if (cluster.command != 0x07) {
|
||||||
|
log.debug 'motion'
|
||||||
|
resultMap.name = 'motion'
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,10 +162,8 @@ private Map parseCatchAllMessage(String description) {
|
|||||||
|
|
||||||
private boolean shouldProcessMessage(cluster) {
|
private boolean shouldProcessMessage(cluster) {
|
||||||
// 0x0B is default response indicating message got through
|
// 0x0B is default response indicating message got through
|
||||||
// 0x07 is bind message
|
|
||||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||||
cluster.command == 0x0B ||
|
cluster.command == 0x0B ||
|
||||||
cluster.command == 0x07 ||
|
|
||||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||||
return !ignoredMessage
|
return !ignoredMessage
|
||||||
}
|
}
|
||||||
@@ -201,9 +210,9 @@ private Map parseIasMessage(String description) {
|
|||||||
def getTemperature(value) {
|
def getTemperature(value) {
|
||||||
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
||||||
if(getTemperatureScale() == "C"){
|
if(getTemperatureScale() == "C"){
|
||||||
return celsius
|
return Math.round(celsius)
|
||||||
} else {
|
} else {
|
||||||
return celsiusToFahrenheit(celsius) as Integer
|
return Math.round(celsiusToFahrenheit(celsius))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,6 +257,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 }}%"
|
||||||
}
|
}
|
||||||
@@ -274,7 +285,8 @@ private Map getTemperatureResult(value) {
|
|||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText,
|
descriptionText: descriptionText,
|
||||||
translatable: true
|
translatable: true,
|
||||||
|
unit: temperatureScale
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,15 +305,7 @@ private Map getMotionResult(value) {
|
|||||||
* PING is used by Device-Watch in attempt to reach the Device
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
* */
|
* */
|
||||||
def ping() {
|
def ping() {
|
||||||
|
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
||||||
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
|
|
||||||
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
|
|
||||||
state.lastActivity = null
|
|
||||||
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
|
||||||
} else {
|
|
||||||
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
|
|
||||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
@@ -315,24 +319,13 @@ def refresh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
sendEvent(name: "checkInterval", value: 14400, displayed: false, data: [protocol: "zigbee"])
|
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||||
|
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||||
|
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
|
|
||||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
// battery minReport 30 seconds, maxReportTime 6 hrs by default
|
||||||
|
return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config
|
||||||
def configCmds = [
|
|
||||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def enrollResponse() {
|
def enrollResponse() {
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ private Map parseCustomMessage(String description) {
|
|||||||
|
|
||||||
private Map parseIasMessage(String description) {
|
private Map parseIasMessage(String description) {
|
||||||
ZoneStatus zs = zigbee.parseZoneStatus(description)
|
ZoneStatus zs = zigbee.parseZoneStatus(description)
|
||||||
return zs.isAlarm1Set() ? getMotionResult('active') : getMotionResult('inactive')
|
return (zs.isAlarm1Set() || zs.isAlarm2Set()) ? getMotionResult('active') : getMotionResult('inactive')
|
||||||
}
|
}
|
||||||
|
|
||||||
def getTemperature(value) {
|
def getTemperature(value) {
|
||||||
@@ -206,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}%"
|
||||||
}
|
}
|
||||||
@@ -226,7 +228,8 @@ private Map getTemperatureResult(value) {
|
|||||||
return [
|
return [
|
||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText
|
descriptionText: descriptionText,
|
||||||
|
unit: temperatureScale
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,13 +255,9 @@ def refresh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
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 configCmds = [
|
||||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
"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
|
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
@@ -267,7 +266,7 @@ def configure() {
|
|||||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||||
]
|
]
|
||||||
return configCmds + refresh() // send refresh cmds as part of config
|
return refresh() + configCmds // send refresh cmds as part of config
|
||||||
}
|
}
|
||||||
|
|
||||||
def enrollResponse() {
|
def enrollResponse() {
|
||||||
|
|||||||
@@ -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 5 mins.
|
||||||
|
Check-in interval is double the value of maxReportTime.
|
||||||
|
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||||
|
Check-in interval = 12 mins
|
||||||
|
|
||||||
|
## 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)
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
|
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") {
|
||||||
|
|
||||||
capability "Three Axis"
|
capability "Three Axis"
|
||||||
capability "Battery"
|
capability "Battery"
|
||||||
@@ -127,13 +127,6 @@ def parse(String description) {
|
|||||||
map = parseIasMessage(description)
|
map = parseIasMessage(description)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporary fix for the case when Device is OFFLINE and is connected again
|
|
||||||
if (state.lastActivity == null){
|
|
||||||
state.lastActivity = now()
|
|
||||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
|
||||||
}
|
|
||||||
state.lastActivity = now()
|
|
||||||
|
|
||||||
def result = map ? createEvent(map) : null
|
def result = map ? createEvent(map) : null
|
||||||
|
|
||||||
if (description?.startsWith('enroll request')) {
|
if (description?.startsWith('enroll request')) {
|
||||||
@@ -154,20 +147,33 @@ private Map parseCatchAllMessage(String description) {
|
|||||||
if (shouldProcessMessage(cluster)) {
|
if (shouldProcessMessage(cluster)) {
|
||||||
switch(cluster.clusterId) {
|
switch(cluster.clusterId) {
|
||||||
case 0x0001:
|
case 0x0001:
|
||||||
resultMap = getBatteryResult(cluster.data.last())
|
// 0x07 - configure reporting
|
||||||
|
if (cluster.command != 0x07) {
|
||||||
|
resultMap = getBatteryResult(cluster.data.last())
|
||||||
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 0xFC02:
|
case 0xFC02:
|
||||||
log.debug 'ACCELERATION'
|
log.debug 'ACCELERATION'
|
||||||
break
|
break
|
||||||
|
|
||||||
case 0x0402:
|
case 0x0402:
|
||||||
log.debug 'TEMP'
|
if (cluster.command == 0x07) {
|
||||||
// temp is last 2 data values. reverse to swap endian
|
if(cluster.data[0] == 0x00) {
|
||||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster
|
||||||
def value = getTemperature(temp)
|
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
resultMap = getTemperatureResult(value)
|
}
|
||||||
break
|
else {
|
||||||
|
log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// temp is last 2 data values. reverse to swap endian
|
||||||
|
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||||
|
def value = getTemperature(temp)
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,10 +182,8 @@ private Map parseCatchAllMessage(String description) {
|
|||||||
|
|
||||||
private boolean shouldProcessMessage(cluster) {
|
private boolean shouldProcessMessage(cluster) {
|
||||||
// 0x0B is default response indicating message got through
|
// 0x0B is default response indicating message got through
|
||||||
// 0x07 is bind message
|
|
||||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||||
cluster.command == 0x0B ||
|
cluster.command == 0x0B ||
|
||||||
cluster.command == 0x07 ||
|
|
||||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||||
return !ignoredMessage
|
return !ignoredMessage
|
||||||
}
|
}
|
||||||
@@ -268,9 +272,9 @@ def updated() {
|
|||||||
def getTemperature(value) {
|
def getTemperature(value) {
|
||||||
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
||||||
if(getTemperatureScale() == "C"){
|
if(getTemperatureScale() == "C"){
|
||||||
return celsius
|
return Math.round(celsius)
|
||||||
} else {
|
} else {
|
||||||
return celsiusToFahrenheit(celsius) as Integer
|
return Math.round(celsiusToFahrenheit(celsius))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,6 +317,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 }}%"
|
||||||
}
|
}
|
||||||
@@ -333,10 +339,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
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,15 +382,7 @@ private getAccelerationResult(numValue) {
|
|||||||
* PING is used by Device-Watch in attempt to reach the Device
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
* */
|
* */
|
||||||
def ping() {
|
def ping() {
|
||||||
|
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
||||||
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
|
|
||||||
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
|
|
||||||
state.lastActivity = null
|
|
||||||
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
|
||||||
} else {
|
|
||||||
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
|
|
||||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
@@ -413,19 +412,22 @@ def refresh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
sendEvent(name: "checkInterval", value: 14400, displayed: false, data: [protocol: "zigbee"])
|
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||||
|
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||||
|
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
|
|
||||||
log.debug "Configuring Reporting"
|
log.debug "Configuring Reporting"
|
||||||
|
|
||||||
def configCmds = enrollResponse() +
|
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||||
zigbee.batteryConfig() +
|
// battery minReport 30 seconds, maxReportTime 6 hrs by default
|
||||||
zigbee.temperatureConfig() +
|
def configCmds = zigbee.batteryConfig() +
|
||||||
|
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]) +
|
||||||
zigbee.configureReporting(0xFC02, 0x0014, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode])
|
zigbee.configureReporting(0xFC02, 0x0014, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode])
|
||||||
|
|
||||||
return configCmds + refresh()
|
return refresh() + configCmds
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEndpointId() {
|
private getEndpointId() {
|
||||||
|
|||||||
@@ -206,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}%"
|
||||||
}
|
}
|
||||||
@@ -223,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
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,12 +277,8 @@ def getTemperature(value) {
|
|||||||
def configure() {
|
def configure() {
|
||||||
sendEvent(name: "checkInterval", value: 7200, displayed: false)
|
sendEvent(name: "checkInterval", value: 7200, displayed: false)
|
||||||
|
|
||||||
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 configCmds = [
|
||||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
"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
|
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||||
@@ -292,7 +291,7 @@ def configure() {
|
|||||||
"zcl global send-me-a-report 0xFC02 2 0x18 30 3600 {01}",
|
"zcl global send-me-a-report 0xFC02 2 0x18 30 3600 {01}",
|
||||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||||
]
|
]
|
||||||
return configCmds + refresh() // send refresh cmds as part of config
|
return refresh() + configCmds // send refresh cmds as part of config
|
||||||
}
|
}
|
||||||
|
|
||||||
def enrollResponse() {
|
def enrollResponse() {
|
||||||
|
|||||||
@@ -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 5 mins.
|
||||||
|
Check-in interval is double the value of maxReportTime.
|
||||||
|
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||||
|
Check-in interval = 12 mins
|
||||||
|
|
||||||
|
## 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)
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
|
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") {
|
||||||
capability "Battery"
|
capability "Battery"
|
||||||
capability "Configuration"
|
capability "Configuration"
|
||||||
capability "Contact Sensor"
|
capability "Contact Sensor"
|
||||||
@@ -92,13 +92,6 @@ def parse(String description) {
|
|||||||
map = parseIasMessage(description)
|
map = parseIasMessage(description)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporary fix for the case when Device is OFFLINE and is connected again
|
|
||||||
if (state.lastActivity == null){
|
|
||||||
state.lastActivity = now()
|
|
||||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
|
||||||
}
|
|
||||||
state.lastActivity = now()
|
|
||||||
|
|
||||||
log.debug "Parse returned $map"
|
log.debug "Parse returned $map"
|
||||||
def result = map ? createEvent(map) : null
|
def result = map ? createEvent(map) : null
|
||||||
|
|
||||||
@@ -116,15 +109,28 @@ private Map parseCatchAllMessage(String description) {
|
|||||||
if (shouldProcessMessage(cluster)) {
|
if (shouldProcessMessage(cluster)) {
|
||||||
switch(cluster.clusterId) {
|
switch(cluster.clusterId) {
|
||||||
case 0x0001:
|
case 0x0001:
|
||||||
resultMap = getBatteryResult(cluster.data.last())
|
// 0x07 - configure reporting
|
||||||
|
if (cluster.command != 0x07) {
|
||||||
|
resultMap = getBatteryResult(cluster.data.last())
|
||||||
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 0x0402:
|
case 0x0402:
|
||||||
log.debug 'TEMP'
|
if (cluster.command == 0x07){
|
||||||
// temp is last 2 data values. reverse to swap endian
|
if (cluster.data[0] == 0x00) {
|
||||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster
|
||||||
def value = getTemperature(temp)
|
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
resultMap = getTemperatureResult(value)
|
}
|
||||||
|
else {
|
||||||
|
log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// temp is last 2 data values. reverse to swap endian
|
||||||
|
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||||
|
def value = getTemperature(temp)
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,10 +140,8 @@ private Map parseCatchAllMessage(String description) {
|
|||||||
|
|
||||||
private boolean shouldProcessMessage(cluster) {
|
private boolean shouldProcessMessage(cluster) {
|
||||||
// 0x0B is default response indicating message got through
|
// 0x0B is default response indicating message got through
|
||||||
// 0x07 is bind message
|
|
||||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||||
cluster.command == 0x0B ||
|
cluster.command == 0x0B ||
|
||||||
cluster.command == 0x07 ||
|
|
||||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||||
return !ignoredMessage
|
return !ignoredMessage
|
||||||
}
|
}
|
||||||
@@ -207,6 +211,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}%"
|
||||||
}
|
}
|
||||||
@@ -226,7 +232,8 @@ private Map getTemperatureResult(value) {
|
|||||||
return [
|
return [
|
||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText
|
descriptionText: descriptionText,
|
||||||
|
unit: temperatureScale
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,15 +252,7 @@ private Map getContactResult(value) {
|
|||||||
* PING is used by Device-Watch in attempt to reach the Device
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
* */
|
* */
|
||||||
def ping() {
|
def ping() {
|
||||||
|
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
||||||
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
|
|
||||||
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
|
|
||||||
state.lastActivity = null
|
|
||||||
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
|
||||||
} else {
|
|
||||||
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
|
|
||||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
@@ -267,23 +266,15 @@ def refresh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
sendEvent(name: "checkInterval", value: 14400, displayed: false, data: [protocol: "zigbee"])
|
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||||
|
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||||
|
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
|
|
||||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
|
||||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||||
def configCmds = [
|
|
||||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
|
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
// battery minReport 30 seconds, maxReportTime 6 hrs by default
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config
|
||||||
|
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 5 mins.
|
||||||
|
Check-in interval is double the value of maxReportTime.
|
||||||
|
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||||
|
Check-in interval = 12 mins
|
||||||
|
|
||||||
|
## 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)
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "SmartSense Temp/Humidity Sensor",namespace: "smartthings", author: "SmartThings", category: "C2") {
|
definition (name: "SmartSense Temp/Humidity Sensor",namespace: "smartthings", author: "SmartThings") {
|
||||||
capability "Configuration"
|
capability "Configuration"
|
||||||
capability "Battery"
|
capability "Battery"
|
||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
@@ -83,13 +83,6 @@ def parse(String description) {
|
|||||||
map = parseCustomMessage(description)
|
map = parseCustomMessage(description)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporary fix for the case when Device is OFFLINE and is connected again
|
|
||||||
if (state.lastActivity == null){
|
|
||||||
state.lastActivity = now()
|
|
||||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
|
||||||
}
|
|
||||||
state.lastActivity = now()
|
|
||||||
|
|
||||||
log.debug "Parse returned $map"
|
log.debug "Parse returned $map"
|
||||||
return map ? createEvent(map) : null
|
return map ? createEvent(map) : null
|
||||||
}
|
}
|
||||||
@@ -100,20 +93,37 @@ private Map parseCatchAllMessage(String description) {
|
|||||||
if (shouldProcessMessage(cluster)) {
|
if (shouldProcessMessage(cluster)) {
|
||||||
switch(cluster.clusterId) {
|
switch(cluster.clusterId) {
|
||||||
case 0x0001:
|
case 0x0001:
|
||||||
resultMap = getBatteryResult(cluster.data.last())
|
// 0x07 - configure reporting
|
||||||
|
if (cluster.command != 0x07) {
|
||||||
|
resultMap = getBatteryResult(cluster.data.last())
|
||||||
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 0x0402:
|
case 0x0402:
|
||||||
// temp is last 2 data values. reverse to swap endian
|
if (cluster.command == 0x07) {
|
||||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
if (cluster.data[0] == 0x00){
|
||||||
def value = getTemperature(temp)
|
log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster
|
||||||
resultMap = getTemperatureResult(value)
|
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
break
|
}
|
||||||
|
else {
|
||||||
|
log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// temp is last 2 data values. reverse to swap endian
|
||||||
|
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||||
|
def value = getTemperature(temp)
|
||||||
|
resultMap = getTemperatureResult(value)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
case 0xFC45:
|
case 0xFC45:
|
||||||
String pctStr = cluster.data[-1, -2].collect { Integer.toHexString(it) }.join('')
|
// 0x07 - configure reporting
|
||||||
String display = Math.round(Integer.valueOf(pctStr, 16) / 100)
|
if (cluster.command != 0x07) {
|
||||||
resultMap = getHumidityResult(display)
|
String pctStr = cluster.data[-1, -2].collect { Integer.toHexString(it) }.join('')
|
||||||
|
String display = Math.round(Integer.valueOf(pctStr, 16) / 100)
|
||||||
|
resultMap = getHumidityResult(display)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,10 +133,8 @@ private Map parseCatchAllMessage(String description) {
|
|||||||
|
|
||||||
private boolean shouldProcessMessage(cluster) {
|
private boolean shouldProcessMessage(cluster) {
|
||||||
// 0x0B is default response indicating message got through
|
// 0x0B is default response indicating message got through
|
||||||
// 0x07 is bind message
|
|
||||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||||
cluster.command == 0x0B ||
|
cluster.command == 0x0B ||
|
||||||
cluster.command == 0x07 ||
|
|
||||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||||
return !ignoredMessage
|
return !ignoredMessage
|
||||||
}
|
}
|
||||||
@@ -214,6 +222,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}%"
|
||||||
}
|
}
|
||||||
@@ -233,65 +243,47 @@ private Map getTemperatureResult(value) {
|
|||||||
return [
|
return [
|
||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText
|
descriptionText: descriptionText,
|
||||||
|
unit: temperatureScale
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map getHumidityResult(value) {
|
private Map getHumidityResult(value) {
|
||||||
log.debug 'Humidity'
|
log.debug 'Humidity'
|
||||||
return [
|
return value ? [name: 'humidity', value: value, unit: '%'] : [:]
|
||||||
name: 'humidity',
|
|
||||||
value: value,
|
|
||||||
unit: '%'
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PING is used by Device-Watch in attempt to reach the Device
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
* */
|
* */
|
||||||
def ping() {
|
def ping() {
|
||||||
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
|
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
||||||
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
|
|
||||||
state.lastActivity = null
|
|
||||||
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
|
||||||
} else {
|
|
||||||
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
|
|
||||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def refresh()
|
def refresh()
|
||||||
{
|
{
|
||||||
log.debug "refresh temperature, humidity, and battery"
|
log.debug "refresh temperature, humidity, and battery"
|
||||||
[
|
return zigbee.readAttribute(0xFC45, 0x0000, ["mfgCode": 0xC2DF]) + // Original firmware
|
||||||
|
zigbee.readAttribute(0xFC45, 0x0000, ["mfgCode": 0x104E]) + // New firmware
|
||||||
"zcl mfg-code 0xC2DF", "delay 1000",
|
zigbee.readAttribute(0x0402, 0x0000) +
|
||||||
"zcl global read 0xFC45 0", "delay 1000",
|
zigbee.readAttribute(0x0001, 0x0020)
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1000",
|
|
||||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
|
||||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20"
|
|
||||||
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
sendEvent(name: "checkInterval", value: 14400, displayed: false, data: [protocol: "zigbee"])
|
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||||
|
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||||
|
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
|
|
||||||
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 refresh() + humidityConfigCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config
|
||||||
}
|
}
|
||||||
|
|
||||||
private hex(value) {
|
private hex(value) {
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.st-ignore
|
||||||
|
README.md
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Tyco Door Window Sensor
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Works with:
|
||||||
|
|
||||||
|
* [Tyco Door Window Sensor](https://support.smartthings.com/hc/en-us/articles/204834100-Tyco-Door-Window-Sensor)
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
* [Capabilities](#capabilities)
|
||||||
|
* [Health](#device-health)
|
||||||
|
* [Battery](#battery-specification)
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
* **Battery** - defines device uses a battery
|
||||||
|
* **Configuration** - _configure()_ command called when device is installed or device preferences updated
|
||||||
|
* **Contact Sensor** - can detect contact (open/close)
|
||||||
|
* **Refresh** - _refresh()_ command for status updates
|
||||||
|
* **Temperature Measurement** - can measure the device temperature
|
||||||
|
* **Health Check** - indicates ability to get device health notifications
|
||||||
|
|
||||||
|
## Device Health
|
||||||
|
|
||||||
|
Contact sensor with maxReportTime of 5 mins.
|
||||||
|
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 = 12 min
|
||||||
|
|
||||||
|
## Battery Specification
|
||||||
|
|
||||||
|
3V CR2032 battery is required.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that either the sensor needs to be reseted or the sensor is out of range.
|
||||||
|
Reset needs to be done by inserting the battery in the sensor and then quickly pressing the adjacent black button 10 times. Pairing should be tried again now.
|
||||||
|
It may happen that sensor is out of range, then 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:
|
||||||
|
* [Tyco Door Window Sensor (MCT-340)](https://support.smartthings.com/hc/en-us/articles/204834100-Tyco-Door-Window-Sensor)
|
||||||
@@ -22,6 +22,7 @@ metadata {
|
|||||||
capability "Contact Sensor"
|
capability "Contact Sensor"
|
||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
capability "Temperature Measurement"
|
capability "Temperature Measurement"
|
||||||
|
capability "Health Check"
|
||||||
|
|
||||||
command "enrollResponse"
|
command "enrollResponse"
|
||||||
|
|
||||||
@@ -213,7 +214,8 @@ private Map getTemperatureResult(value) {
|
|||||||
return [
|
return [
|
||||||
name: 'temperature',
|
name: 'temperature',
|
||||||
value: value,
|
value: value,
|
||||||
descriptionText: descriptionText
|
descriptionText: descriptionText,
|
||||||
|
unit: temperatureScale
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,44 +230,42 @@ private Map getContactResult(value) {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
|
* */
|
||||||
|
def ping() {
|
||||||
|
return zigbee.readAttribute(0x0402, 0x0000) // Read the Temperature Cluster
|
||||||
|
}
|
||||||
|
|
||||||
def refresh()
|
def refresh()
|
||||||
{
|
{
|
||||||
log.debug "Refreshing Temperature and Battery"
|
log.debug "Refreshing Temperature and Battery"
|
||||||
[
|
def refreshCmds = [
|
||||||
|
|
||||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20"
|
"st rattr 0x${device.deviceNetworkId} 1 1 0x20"
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
return refreshCmds + enrollResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
|
// Device-Watch allows 2 check-in misses from device
|
||||||
|
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
|
|
||||||
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 = [
|
||||||
"delay 1000",
|
"delay 1000",
|
||||||
|
|
||||||
"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 1500",
|
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", "delay 200",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
|
||||||
|
|
||||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200",
|
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
|
||||||
|
|
||||||
|
|
||||||
//"raw 0x500 {01 23 00 00 00}", "delay 200",
|
//"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||||
//"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
//"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}",
|
|
||||||
|
|
||||||
"delay 500"
|
|
||||||
]
|
]
|
||||||
return configCmds + enrollResponse() + refresh() // send refresh cmds as part of config
|
return enrollCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) + refresh() // send refresh cmds as part of config
|
||||||
}
|
}
|
||||||
|
|
||||||
def enrollResponse() {
|
def enrollResponse() {
|
||||||
|
|||||||
@@ -89,14 +89,8 @@ def parse(String description) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Map parseIasButtonMessage(String description) {
|
private Map parseIasButtonMessage(String description) {
|
||||||
int zoneInt = Integer.parseInt((description - "zone status 0x"), 16)
|
def zs = zigbee.parseZoneStatus(description)
|
||||||
if (zoneInt & 0x02) {
|
return zs.isAlarm2Set() ? getButtonResult("press") : getButtonResult("release")
|
||||||
resultMap = getButtonResult('press')
|
|
||||||
} else {
|
|
||||||
resultMap = getButtonResult('release')
|
|
||||||
}
|
|
||||||
|
|
||||||
return resultMap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map getBatteryResult(rawValue) {
|
private Map getBatteryResult(rawValue) {
|
||||||
|
|||||||
@@ -93,5 +93,5 @@ def refresh() {
|
|||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
log.debug "Configuring Reporting and Bindings."
|
log.debug "Configuring Reporting and Bindings."
|
||||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh()
|
refresh()
|
||||||
}
|
}
|
||||||
|
|||||||
2
devicetypes/smartthings/zigbee-dimmer.src/.st-ignore
Normal file
2
devicetypes/smartthings/zigbee-dimmer.src/.st-ignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.st-ignore
|
||||||
|
README.md
|
||||||
36
devicetypes/smartthings/zigbee-dimmer.src/README.md
Normal file
36
devicetypes/smartthings/zigbee-dimmer.src/README.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# OSRAM Lightify LED On/Off/Dim
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Works with:
|
||||||
|
|
||||||
|
* [OSRAM Lightify LED On/Off/Dim](https://shop.smartthings.com/#!/products/osram-led-smart-bulb-on-off-dim)
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
* [Capabilities](#capabilities)
|
||||||
|
* [Health](#device-health)
|
||||||
|
* [Battery](#battery-specification)
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
* **Actuator** - represents that a Device has commands
|
||||||
|
* **Configuration** - _configure()_ command called when device is installed or device preferences updated
|
||||||
|
* **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 C1 Zigbee dimmer with maxReportTime of 5 mins.
|
||||||
|
Check-in interval is double the value of maxReportTime.
|
||||||
|
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||||
|
Check-in interval = 12 mins
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
Other troubleshooting tips are listed as follows:
|
||||||
|
* [Troubleshooting:](https://support.smartthings.com/hc/en-us/articles/207191763-OSRAM-LIGHTIFY-LED-Smart-Connected-Light-A19-On-Off-Dim)
|
||||||
@@ -54,20 +54,27 @@ def parse(String description) {
|
|||||||
|
|
||||||
def event = zigbee.getEvent(description)
|
def event = zigbee.getEvent(description)
|
||||||
if (event) {
|
if (event) {
|
||||||
// Temporary fix for the case when Device is OFFLINE and is connected again
|
|
||||||
if (state.lastActivity == null){
|
|
||||||
state.lastActivity = now()
|
|
||||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
|
||||||
}
|
|
||||||
state.lastActivity = now()
|
|
||||||
if (event.name=="level" && event.value==0) {}
|
if (event.name=="level" && event.value==0) {}
|
||||||
else {
|
else {
|
||||||
sendEvent(event)
|
sendEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
def cluster = zigbee.parse(description)
|
||||||
log.debug zigbee.parseDescriptionAsMap(description)
|
|
||||||
|
if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) {
|
||||||
|
if (cluster.data[0] == 0x00) {
|
||||||
|
log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster
|
||||||
|
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||||
|
log.debug "${cluster}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,24 +93,19 @@ def setLevel(value) {
|
|||||||
* PING is used by Device-Watch in attempt to reach the Device
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
* */
|
* */
|
||||||
def ping() {
|
def ping() {
|
||||||
|
return zigbee.onOffRefresh()
|
||||||
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
|
|
||||||
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
|
|
||||||
state.lastActivity = null
|
|
||||||
return zigbee.onOffRefresh()
|
|
||||||
} else {
|
|
||||||
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
|
|
||||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig()
|
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig(0, 300) + zigbee.levelConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
log.debug "Configuring Reporting and Bindings."
|
log.debug "Configuring Reporting and Bindings."
|
||||||
// Enrolls device to Device-Watch with 3 x Reporting interval 30min
|
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||||
sendEvent(name: "checkInterval", value: 1800, displayed: false, data: [protocol: "zigbee"])
|
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
|
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
|
|
||||||
|
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||||
|
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig(0, 300) + zigbee.levelConfig()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,8 @@
|
|||||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD210 PB DB", deviceJoinName: "Yale Push Button Deadbolt Lock"
|
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD210 PB DB", deviceJoinName: "Yale Push Button Deadbolt Lock"
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD220/240 TSDB", deviceJoinName: "Yale Touch Screen Deadbolt Lock"
|
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD220/240 TSDB", deviceJoinName: "Yale Touch Screen Deadbolt Lock"
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRL210 PB LL", deviceJoinName: "Yale Push Button Lever Lock"
|
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRL210 PB LL", deviceJoinName: "Yale Push Button Lever Lock"
|
||||||
}
|
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD226/246 TSDB", deviceJoinName: "Yale Touch Screen Deadbolt Lock"
|
||||||
|
}
|
||||||
|
|
||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
multiAttributeTile(name:"toggle", type:"generic", width:6, height:4){
|
multiAttributeTile(name:"toggle", type:"generic", width:6, height:4){
|
||||||
@@ -89,7 +90,7 @@ def configure() {
|
|||||||
zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING,
|
zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING,
|
||||||
TYPE_U8, 600, 21600, 0x01)
|
TYPE_U8, 600, 21600, 0x01)
|
||||||
log.info "configure() --- cmds: $cmds"
|
log.info "configure() --- cmds: $cmds"
|
||||||
return cmds + refresh() // send refresh cmds as part of config
|
return refresh() + cmds // send refresh cmds as part of config
|
||||||
}
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2016 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
* Date: 2016-01-19
|
||||||
|
*
|
||||||
|
* This DTH should serve as the generic DTH to handle RGB ZigBee HA devices (For color bulbs with no color temperature)
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "ZigBee RGB Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Color Control"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Polling"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Switch Level"
|
||||||
|
capability "Health Check"
|
||||||
|
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenspot RGB"
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Gardenspot RGB"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles(scale: 2) {
|
||||||
|
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||||
|
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||||
|
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
}
|
||||||
|
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||||
|
attributeState "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
tileAttribute ("device.color", key: "COLOR_CONTROL") {
|
||||||
|
attributeState "color", action:"color control.setColor"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["switch"])
|
||||||
|
details(["switch", "refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Globals
|
||||||
|
private getATTRIBUTE_HUE() { 0x0000 }
|
||||||
|
private getATTRIBUTE_SATURATION() { 0x0001 }
|
||||||
|
private getHUE_COMMAND() { 0x00 }
|
||||||
|
private getSATURATION_COMMAND() { 0x03 }
|
||||||
|
private getCOLOR_CONTROL_CLUSTER() { 0x0300 }
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "description is $description"
|
||||||
|
|
||||||
|
def event = zigbee.getEvent(description)
|
||||||
|
if (event) {
|
||||||
|
log.debug event
|
||||||
|
if (event.name=="level" && event.value==0) {}
|
||||||
|
else {
|
||||||
|
sendEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
def zigbeeMap = zigbee.parseDescriptionAsMap(description)
|
||||||
|
def cluster = zigbee.parse(description)
|
||||||
|
|
||||||
|
if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) {
|
||||||
|
if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute
|
||||||
|
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||||
|
sendEvent(name: "hue", value: hueValue, descriptionText: "Color has changed")
|
||||||
|
}
|
||||||
|
else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute
|
||||||
|
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||||
|
sendEvent(name: "saturation", value: saturationValue, descriptionText: "Color has changed", displayed: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) {
|
||||||
|
if (cluster.data[0] == 0x00){
|
||||||
|
log.debug "ON/OFF REPORTING CONFIG RESPONSE: $cluster"
|
||||||
|
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.info "DID NOT PARSE MESSAGE for description : $description"
|
||||||
|
log.debug zigbeeMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
zigbee.on()
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
zigbee.off()
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
|
* */
|
||||||
|
def ping() {
|
||||||
|
return zigbee.onOffRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01)
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
log.debug "Configuring Reporting and Bindings."
|
||||||
|
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||||
|
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||||
|
sendEvent(name: "checkInterval", value: 3 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
|
|
||||||
|
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||||
|
zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + 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.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value) {
|
||||||
|
zigbee.setLevel(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setColor(value){
|
||||||
|
log.trace "setColor($value)"
|
||||||
|
zigbee.on() + setHue(value.hue) + "delay 500" + setSaturation(value.saturation)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setHue(value) {
|
||||||
|
def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||||
|
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setSaturation(value) {
|
||||||
|
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||||
|
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + "delay 1000" + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||||
|
}
|
||||||
2
devicetypes/smartthings/zigbee-rgbw-bulb.src/.st-ignore
Normal file
2
devicetypes/smartthings/zigbee-rgbw-bulb.src/.st-ignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.st-ignore
|
||||||
|
README.md
|
||||||
42
devicetypes/smartthings/zigbee-rgbw-bulb.src/README.md
Normal file
42
devicetypes/smartthings/zigbee-rgbw-bulb.src/README.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# OSRAM LIGHTIFY LED RGBW Bulb
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Works with:
|
||||||
|
|
||||||
|
* [OSRAM LIGHTIFY LED RGBW Bulb](https://support.smartthings.com/hc/en-us/articles/207728173-OSRAM-LIGHTIFY-LED-Smart-Connected-Light-A19-RGBW)
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
* [Capabilities](#capabilities)
|
||||||
|
* [Health](#device-health)
|
||||||
|
* [Battery](#battery-specification)
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
* **Actuator** - It represents that a device has commands.
|
||||||
|
* **Color Control** - It represents that the color attributes of a device can be controlled (hue, saturation, color value).
|
||||||
|
* **Color Temperature** - It represents color temperature capability measured in degree Kelvin.
|
||||||
|
* **Polling** - It represents that a device can be polled.
|
||||||
|
* **Switch** - can detect state (possible values: on/off)
|
||||||
|
* **Switch Level** - can detect current light level (0-100 in percent)
|
||||||
|
* **Configuration** - _configure()_ command called when device is installed or device preferences updated
|
||||||
|
* **Refresh** - _refresh()_ command for status updates
|
||||||
|
* **Health Check** - indicates ability to get device health notifications
|
||||||
|
|
||||||
|
|
||||||
|
## Device Health
|
||||||
|
|
||||||
|
A Category C6 OSRAM LIGHTIFY LED RGBW Bulb with maxReportTime of 5 mins.
|
||||||
|
Check-in interval is double the value of maxReportTime.
|
||||||
|
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||||
|
Check-in interval = 12 mins
|
||||||
|
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
It may also happen that you need to reset the device.
|
||||||
|
Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link:
|
||||||
|
* [Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/207728173-OSRAM-LIGHTIFY-LED-Smart-Connected-Light-A19-RGBW)
|
||||||
@@ -85,12 +85,6 @@ def parse(String description) {
|
|||||||
def event = zigbee.getEvent(description)
|
def event = zigbee.getEvent(description)
|
||||||
if (event) {
|
if (event) {
|
||||||
log.debug event
|
log.debug event
|
||||||
// Temporary fix for the case when Device is OFFLINE and is connected again
|
|
||||||
if (state.lastActivity == null){
|
|
||||||
state.lastActivity = now()
|
|
||||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
|
||||||
}
|
|
||||||
state.lastActivity = now()
|
|
||||||
if (event.name=="level" && event.value==0) {}
|
if (event.name=="level" && event.value==0) {}
|
||||||
else {
|
else {
|
||||||
if (event.name=="colorTemperature") {
|
if (event.name=="colorTemperature") {
|
||||||
@@ -101,20 +95,30 @@ def parse(String description) {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
def zigbeeMap = zigbee.parseDescriptionAsMap(description)
|
def zigbeeMap = zigbee.parseDescriptionAsMap(description)
|
||||||
log.trace "zigbeeMap : $zigbeeMap"
|
def cluster = zigbee.parse(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 if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) {
|
||||||
|
if (cluster.data[0] == 0x00){
|
||||||
|
log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster
|
||||||
|
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log.info "DID NOT PARSE MESSAGE for description : $description"
|
log.info "DID NOT PARSE MESSAGE for description : $description"
|
||||||
|
log.debug zigbeeMap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,26 +134,21 @@ def off() {
|
|||||||
* PING is used by Device-Watch in attempt to reach the Device
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
* */
|
* */
|
||||||
def ping() {
|
def ping() {
|
||||||
|
return zigbee.onOffRefresh()
|
||||||
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
|
|
||||||
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
|
|
||||||
state.lastActivity = null
|
|
||||||
return zigbee.onOffRefresh()
|
|
||||||
} else {
|
|
||||||
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
|
|
||||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
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)
|
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
log.debug "Configuring Reporting and Bindings."
|
log.debug "Configuring Reporting and Bindings."
|
||||||
// Enrolls device to Device-Watch with 3 x Reporting interval 30min
|
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||||
sendEvent(name: "checkInterval", value: 1800, displayed: false, data: [protocol: "zigbee"])
|
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||||
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)
|
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
|
|
||||||
|
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||||
|
refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
def setColorTemperature(value) {
|
def setColorTemperature(value) {
|
||||||
@@ -190,5 +189,5 @@ def setHue(value) {
|
|||||||
|
|
||||||
def setSaturation(value) {
|
def setSaturation(value) {
|
||||||
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||||
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") //payload-> sat value, transition time
|
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + "delay 1000" + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,5 +82,5 @@ def refresh() {
|
|||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
log.debug "Configuring Reporting and Bindings."
|
log.debug "Configuring Reporting and Bindings."
|
||||||
zigbee.onOffConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() + zigbee.onOffRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh()
|
refresh()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,5 +78,5 @@ def refresh() {
|
|||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
log.debug "Configuring Reporting and Bindings."
|
log.debug "Configuring Reporting and Bindings."
|
||||||
zigbee.onOffConfig() + zigbee.onOffRefresh()
|
zigbee.onOffRefresh() + zigbee.onOffConfig()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,10 +134,5 @@ def refresh() {
|
|||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
log.debug "Configuring Reporting and Bindings."
|
log.debug "Configuring Reporting and Bindings."
|
||||||
zigbee.onOffConfig() +
|
refresh()
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
.st-ignore
|
||||||
|
README.md
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# OSRAM Lightify Tunable 60 White
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Works with:
|
||||||
|
|
||||||
|
* [OSRAM Lightify Tunable 60 White](http://www.osram.com/osram_com/tools-and-services/tools/lightify---smart-connected-light/lightify-for-home---what-is-light-to-you/lightify-products/lightify-classic-a60-tunable-white/index.jsp)
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
* [Capabilities](#capabilities)
|
||||||
|
* [Health](#device-health)
|
||||||
|
* [Battery](#battery-specification)
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
* **Actuator** - represents that a Device has commands
|
||||||
|
* **Color Temperature** - represents color temperature, measured in degrees Kelvin.
|
||||||
|
* **Configuration** - _configure()_ command called when device is installed or device preferences updated.
|
||||||
|
* **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 C1 OSRAM Lightify Tunable 60 White with maxReportTime of 5 mins.
|
||||||
|
Check-in interval is double the value of maxReportTime.
|
||||||
|
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||||
|
Check-in interval = 12 mins
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
Other troubleshooting tips are listed as follows:
|
||||||
|
* [Troubleshooting:](https://support.smartthings.com/hc/en-us/articles/204576454-OSRAM-LIGHTIFY-Tunable-White-60-Bulb)
|
||||||
@@ -36,6 +36,7 @@ metadata {
|
|||||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY RT Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Recessed Kit RT 5/6 Tunable White"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "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
|
||||||
@@ -73,12 +74,6 @@ 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) {
|
||||||
// Temporary fix for the case when Device is OFFLINE and is connected again
|
|
||||||
if (state.lastActivity == null){
|
|
||||||
state.lastActivity = now()
|
|
||||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
|
||||||
}
|
|
||||||
state.lastActivity = now()
|
|
||||||
if (event.name=="level" && event.value==0) {}
|
if (event.name=="level" && event.value==0) {}
|
||||||
else {
|
else {
|
||||||
if (event.name=="colorTemperature") {
|
if (event.name=="colorTemperature") {
|
||||||
@@ -88,8 +83,21 @@ def parse(String description) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
def cluster = zigbee.parse(description)
|
||||||
log.debug zigbee.parseDescriptionAsMap(description)
|
|
||||||
|
if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) {
|
||||||
|
if (cluster.data[0] == 0x00) {
|
||||||
|
log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster
|
||||||
|
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||||
|
log.debug "${cluster}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,26 +117,21 @@ def setLevel(value) {
|
|||||||
* PING is used by Device-Watch in attempt to reach the Device
|
* PING is used by Device-Watch in attempt to reach the Device
|
||||||
* */
|
* */
|
||||||
def ping() {
|
def ping() {
|
||||||
|
return zigbee.onOffRefresh()
|
||||||
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
|
|
||||||
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
|
|
||||||
state.lastActivity = null
|
|
||||||
return zigbee.onOffRefresh()
|
|
||||||
} else {
|
|
||||||
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
|
|
||||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig()
|
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
log.debug "Configuring Reporting and Bindings."
|
log.debug "Configuring Reporting and Bindings."
|
||||||
// Enrolls device to Device-Watch with 3 x Reporting interval 30min
|
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||||
sendEvent(name: "checkInterval", value: 1800, displayed: false, data: [protocol: "zigbee"])
|
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
|
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||||
|
|
||||||
|
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||||
|
refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
def setColorTemperature(value) {
|
def setColorTemperature(value) {
|
||||||
|
|||||||
134
devicetypes/smartthings/zll-rgb-bulb.src/zll-rgb-bulb.groovy
Normal file
134
devicetypes/smartthings/zll-rgb-bulb.src/zll-rgb-bulb.groovy
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2016 SmartThings
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "ZLL RGB Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Color Control"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Polling"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Switch Level"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles(scale: 2) {
|
||||||
|
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||||
|
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||||
|
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
}
|
||||||
|
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||||
|
attributeState "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
tileAttribute ("device.color", key: "COLOR_CONTROL") {
|
||||||
|
attributeState "color", action:"color control.setColor"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["switch"])
|
||||||
|
details(["switch", "refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Globals
|
||||||
|
private getATTRIBUTE_HUE() { 0x0000 }
|
||||||
|
private getATTRIBUTE_SATURATION() { 0x0001 }
|
||||||
|
private getHUE_COMMAND() { 0x00 }
|
||||||
|
private getSATURATION_COMMAND() { 0x03 }
|
||||||
|
private getCOLOR_CONTROL_CLUSTER() { 0x0300 }
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "description is $description"
|
||||||
|
|
||||||
|
def finalResult = zigbee.getEvent(description)
|
||||||
|
if (finalResult) {
|
||||||
|
log.debug finalResult
|
||||||
|
sendEvent(finalResult)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
def zigbeeMap = zigbee.parseDescriptionAsMap(description)
|
||||||
|
log.trace "zigbeeMap : $zigbeeMap"
|
||||||
|
|
||||||
|
if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) {
|
||||||
|
if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute
|
||||||
|
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 360)
|
||||||
|
sendEvent(name: "hue", value: hueValue, displayed:false)
|
||||||
|
}
|
||||||
|
else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute
|
||||||
|
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||||
|
sendEvent(name: "saturation", value: saturationValue, displayed:false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.info "DID NOT PARSE MESSAGE for description : $description"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
zigbee.on() + ["delay 1500"] + zigbee.onOffRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
zigbee.off() + ["delay 1500"] + zigbee.onOffRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
refreshAttributes() + configureAttributes()
|
||||||
|
}
|
||||||
|
|
||||||
|
def poll() {
|
||||||
|
refreshAttributes()
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
log.debug "Configuring Reporting and Bindings."
|
||||||
|
configureAttributes() + refreshAttributes()
|
||||||
|
}
|
||||||
|
|
||||||
|
def configureAttributes() {
|
||||||
|
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01)
|
||||||
|
}
|
||||||
|
|
||||||
|
def refreshAttributes() {
|
||||||
|
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value) {
|
||||||
|
zigbee.setLevel(value) + ["delay 1500"] + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report
|
||||||
|
}
|
||||||
|
|
||||||
|
def setColor(value){
|
||||||
|
log.trace "setColor($value)"
|
||||||
|
zigbee.on() + setHue(value.hue) + ["delay 300"] + setSaturation(value.saturation) + ["delay 2000"] + refreshAttributes()
|
||||||
|
}
|
||||||
|
|
||||||
|
def setHue(value) {
|
||||||
|
def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||||
|
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setSaturation(value) {
|
||||||
|
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||||
|
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) //payload-> sat value, transition time
|
||||||
|
}
|
||||||
@@ -123,7 +123,7 @@ def configureAttributes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def refreshAttributes() {
|
def refreshAttributes() {
|
||||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.readAttribute(0x0300, 0x00) + zigbee.readAttribute(0x0300, ATTRIBUTE_HUE) + zigbee.readAttribute(0x0300, ATTRIBUTE_SATURATION)
|
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||||
}
|
}
|
||||||
|
|
||||||
def setColorTemperature(value) {
|
def setColorTemperature(value) {
|
||||||
@@ -141,10 +141,10 @@ def setColor(value){
|
|||||||
|
|
||||||
def setHue(value) {
|
def setHue(value) {
|
||||||
def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||||
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5)
|
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
def setSaturation(value) {
|
def setSaturation(value) {
|
||||||
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||||
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") //payload-> sat value, transition time
|
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) //payload-> sat value, transition time
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,16 @@ void updateSwitch() {
|
|||||||
private void updateAll(devices) {
|
private void updateAll(devices) {
|
||||||
def command = request.JSON?.command
|
def command = request.JSON?.command
|
||||||
if (command) {
|
if (command) {
|
||||||
devices."$command"()
|
switch(command) {
|
||||||
|
case "on":
|
||||||
|
devices.on()
|
||||||
|
break
|
||||||
|
case "off":
|
||||||
|
devices.off()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
httpError(403, "Access denied. This command is not supported by current capability.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +86,16 @@ private void update(devices) {
|
|||||||
if (!device) {
|
if (!device) {
|
||||||
httpError(404, "Device not found")
|
httpError(404, "Device not found")
|
||||||
} else {
|
} else {
|
||||||
device."$command"()
|
switch(command) {
|
||||||
|
case "on":
|
||||||
|
device.on()
|
||||||
|
break
|
||||||
|
case "off":
|
||||||
|
device.off()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
httpError(403, "Access denied. This command is not supported by current capability.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ def authPage() {
|
|||||||
if (canInstallLabs()) {
|
if (canInstallLabs()) {
|
||||||
|
|
||||||
def redirectUrl = getBuildRedirectUrl()
|
def redirectUrl = getBuildRedirectUrl()
|
||||||
log.debug "Redirect url = ${redirectUrl}"
|
// log.debug "Redirect url = ${redirectUrl}"
|
||||||
|
|
||||||
if (state.authToken) {
|
if (state.authToken) {
|
||||||
description = "Tap 'Next' to proceed"
|
description = "Tap 'Next' to proceed"
|
||||||
@@ -113,13 +113,13 @@ def oauthInitUrl() {
|
|||||||
scope: "read_station"
|
scope: "read_station"
|
||||||
]
|
]
|
||||||
|
|
||||||
log.debug "REDIRECT URL: ${getVendorAuthPath() + toQueryString(oauthParams)}"
|
// log.debug "REDIRECT URL: ${getVendorAuthPath() + toQueryString(oauthParams)}"
|
||||||
|
|
||||||
redirect (location: getVendorAuthPath() + toQueryString(oauthParams))
|
redirect (location: getVendorAuthPath() + toQueryString(oauthParams))
|
||||||
}
|
}
|
||||||
|
|
||||||
def callback() {
|
def callback() {
|
||||||
log.debug "callback()>> params: $params, params.code ${params.code}"
|
// log.debug "callback()>> params: $params, params.code ${params.code}"
|
||||||
|
|
||||||
def code = params.code
|
def code = params.code
|
||||||
def oauthState = params.state
|
def oauthState = params.state
|
||||||
@@ -135,7 +135,7 @@ def callback() {
|
|||||||
scope: "read_station"
|
scope: "read_station"
|
||||||
]
|
]
|
||||||
|
|
||||||
log.debug "TOKEN URL: ${getVendorTokenPath() + toQueryString(tokenParams)}"
|
// log.debug "TOKEN URL: ${getVendorTokenPath() + toQueryString(tokenParams)}"
|
||||||
|
|
||||||
def tokenUrl = getVendorTokenPath()
|
def tokenUrl = getVendorTokenPath()
|
||||||
def params = [
|
def params = [
|
||||||
@@ -144,7 +144,7 @@ def callback() {
|
|||||||
body: tokenParams
|
body: tokenParams
|
||||||
]
|
]
|
||||||
|
|
||||||
log.debug "PARAMS: ${params}"
|
// log.debug "PARAMS: ${params}"
|
||||||
|
|
||||||
httpPost(params) { resp ->
|
httpPost(params) { resp ->
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ def callback() {
|
|||||||
state.refreshToken = data.refresh_token
|
state.refreshToken = data.refresh_token
|
||||||
state.authToken = data.access_token
|
state.authToken = data.access_token
|
||||||
state.tokenExpires = now() + (data.expires_in * 1000)
|
state.tokenExpires = now() + (data.expires_in * 1000)
|
||||||
log.debug "swapped token: $resp.data"
|
// log.debug "swapped token: $resp.data"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,7 +292,7 @@ def refreshToken() {
|
|||||||
|
|
||||||
response.data.each {key, value ->
|
response.data.each {key, value ->
|
||||||
def data = slurper.parseText(key);
|
def data = slurper.parseText(key);
|
||||||
log.debug "Data: $data"
|
// log.debug "Data: $data"
|
||||||
|
|
||||||
state.refreshToken = data.refresh_token
|
state.refreshToken = data.refresh_token
|
||||||
state.accessToken = data.access_token
|
state.accessToken = data.access_token
|
||||||
@@ -387,18 +387,18 @@ def getDeviceList() {
|
|||||||
state.deviceDetail = [:]
|
state.deviceDetail = [:]
|
||||||
state.deviceState = [:]
|
state.deviceState = [:]
|
||||||
|
|
||||||
apiGet("/api/devicelist") { response ->
|
apiGet("/api/getstationsdata") { response ->
|
||||||
response.data.body.devices.each { value ->
|
response.data.body.devices.each { value ->
|
||||||
def key = value._id
|
def key = value._id
|
||||||
deviceList[key] = "${value.station_name}: ${value.module_name}"
|
deviceList[key] = "${value.station_name}: ${value.module_name}"
|
||||||
state.deviceDetail[key] = value
|
state.deviceDetail[key] = value
|
||||||
state.deviceState[key] = value.dashboard_data
|
state.deviceState[key] = value.dashboard_data
|
||||||
}
|
value.modules.each { value2 ->
|
||||||
response.data.body.modules.each { value ->
|
def key2 = value2._id
|
||||||
def key = value._id
|
deviceList[key2] = "${value.station_name}: ${value2.module_name}"
|
||||||
deviceList[key] = "${state.deviceDetail[value.main_device].station_name}: ${value.module_name}"
|
state.deviceDetail[key2] = value2
|
||||||
state.deviceDetail[key] = value
|
state.deviceState[key2] = value2.dashboard_data
|
||||||
state.deviceState[key] = value.dashboard_data
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Smart Windows
|
* Smart Windows
|
||||||
* Compares two temperatures – indoor vs outdoor, for example – then sends an alert if windows are open (or closed!).
|
* Compares two temperatures – indoor vs outdoor, for example – then sends an alert if windows are open (or closed!).
|
||||||
*
|
*
|
||||||
* Copyright 2014 Eric Gideon
|
* Copyright 2014 Eric Gideon
|
||||||
*
|
*
|
||||||
* Based in part on the "When it's going to rain" SmartApp by the SmartThings team,
|
* Based in part on the "When it's going to rain" SmartApp by the SmartThings team,
|
||||||
@@ -21,13 +21,18 @@ definition(
|
|||||||
name: "Smart Windows",
|
name: "Smart Windows",
|
||||||
namespace: "egid",
|
namespace: "egid",
|
||||||
author: "Eric Gideon",
|
author: "Eric Gideon",
|
||||||
description: "Compares two temperatures – indoor vs outdoor, for example – then sends an alert if windows are open (or closed!). If you don't use an external temperature device, your zipcode will be used instead.",
|
description: "Compares two temperatures – indoor vs outdoor, for example – then sends an alert if windows are open (or closed!). If you don't use an external temperature device, your location will be used instead.",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn.png",
|
iconUrl: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn@2x.png"
|
iconX2Url: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn@2x.png"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
|
|
||||||
|
if (!(location.zipCode || ( location.latitude && location.longitude )) && location.channelName == 'samsungtv') {
|
||||||
|
section { paragraph title: "Note:", "Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location." }
|
||||||
|
}
|
||||||
|
|
||||||
section( "Set the temperature range for your comfort zone..." ) {
|
section( "Set the temperature range for your comfort zone..." ) {
|
||||||
input "minTemp", "number", title: "Minimum temperature"
|
input "minTemp", "number", title: "Minimum temperature"
|
||||||
input "maxTemp", "number", title: "Maximum temperature"
|
input "maxTemp", "number", title: "Maximum temperature"
|
||||||
@@ -39,9 +44,11 @@ preferences {
|
|||||||
input "inTemp", "capability.temperatureMeasurement", title: "Indoor"
|
input "inTemp", "capability.temperatureMeasurement", title: "Indoor"
|
||||||
input "outTemp", "capability.temperatureMeasurement", title: "Outdoor (optional)", required: false
|
input "outTemp", "capability.temperatureMeasurement", title: "Outdoor (optional)", required: false
|
||||||
}
|
}
|
||||||
section( "Set your location" ) {
|
|
||||||
input "zipCode", "text", title: "Zip code"
|
if (location.channelName != 'samsungtv') {
|
||||||
}
|
section( "Set your location" ) { input "zipCode", "text", title: "Zip code" }
|
||||||
|
}
|
||||||
|
|
||||||
section( "Notifications" ) {
|
section( "Notifications" ) {
|
||||||
input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false
|
input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false
|
||||||
input "retryPeriod", "number", title: "Minutes between notifications:"
|
input "retryPeriod", "number", title: "Minutes between notifications:"
|
||||||
@@ -72,7 +79,7 @@ def temperatureHandler(evt) {
|
|||||||
|
|
||||||
def currentInTemp = evt.doubleValue
|
def currentInTemp = evt.doubleValue
|
||||||
def openWindows = sensors.findAll { it?.latestValue("contact") == 'open' }
|
def openWindows = sensors.findAll { it?.latestValue("contact") == 'open' }
|
||||||
|
|
||||||
log.trace "Temp event: $evt"
|
log.trace "Temp event: $evt"
|
||||||
log.info "In: $currentInTemp; Out: $currentOutTemp"
|
log.info "In: $currentInTemp; Out: $currentOutTemp"
|
||||||
|
|
||||||
@@ -98,7 +105,7 @@ def temperatureHandler(evt) {
|
|||||||
if ( currentOutTemp < maxTemp && !openWindows ) {
|
if ( currentOutTemp < maxTemp && !openWindows ) {
|
||||||
send( "Open some windows to cool down the house! Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." )
|
send( "Open some windows to cool down the house! Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." )
|
||||||
} else if ( currentOutTemp > maxTemp && openWindows ) {
|
} else if ( currentOutTemp > maxTemp && openWindows ) {
|
||||||
send( "It's gotten warmer outside! You should close these windows: ${openWindows.join(', ')}. Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." )
|
send( "It's gotten warmer outside! You should close these windows: ${openWindows.join(', ')}. Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." )
|
||||||
} else {
|
} else {
|
||||||
log.debug "No notifications sent. Everything is in the right place."
|
log.debug "No notifications sent. Everything is in the right place."
|
||||||
}
|
}
|
||||||
@@ -125,7 +132,11 @@ def temperatureHandler(evt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def weatherCheck() {
|
def weatherCheck() {
|
||||||
def json = getWeatherFeature("conditions", zipCode)
|
def json
|
||||||
|
if (location.channelName != 'samsungtv')
|
||||||
|
json = getWeatherFeature("conditions", zipCode)
|
||||||
|
else
|
||||||
|
json = getWeatherFeature("conditions")
|
||||||
def currentTemp = json?.current_observation?.temp_f
|
def currentTemp = json?.current_observation?.temp_f
|
||||||
|
|
||||||
if ( currentTemp ) {
|
if ( currentTemp ) {
|
||||||
@@ -150,4 +161,4 @@ private send(msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.info msg
|
log.info msg
|
||||||
}
|
}
|
||||||
|
|||||||
253
smartapps/gideon-api/gideon.src/gideon.groovy
Normal file
253
smartapps/gideon-api/gideon.src/gideon.groovy
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
/**
|
||||||
|
* Gideon
|
||||||
|
*
|
||||||
|
* Copyright 2016 Nicola Russo
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||||
|
* in compliance with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||||
|
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
definition(
|
||||||
|
name: "Gideon",
|
||||||
|
namespace: "gideon.api",
|
||||||
|
author: "Braindrain Solutions",
|
||||||
|
description: "Gideon AI Smart app allows you to connect and control all of your SmartThings devices through the Gideon AI app, making your SmartThings devices even smarter.",
|
||||||
|
category: "Family",
|
||||||
|
iconUrl: "http://s33.postimg.org/t77u7y7v3/logo.png",
|
||||||
|
iconX2Url: "http://s33.postimg.org/t77u7y7v3/logo.png",
|
||||||
|
iconX3Url: "http://s33.postimg.org/t77u7y7v3/logo.png",
|
||||||
|
oauth: [displayName: "Gideon AI API", displayLink: "gideon.ai"])
|
||||||
|
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
section("Control these switches...") {
|
||||||
|
input "switches", "capability.switch", multiple:true
|
||||||
|
}
|
||||||
|
section("Control these motion sensors...") {
|
||||||
|
input "motions", "capability.motionSensor", multiple:true
|
||||||
|
}
|
||||||
|
section("Control these presence sensors...") {
|
||||||
|
input "presence_sensors", "capability.presenceSensor", multiple:true
|
||||||
|
}
|
||||||
|
section("Control these outlets...") {
|
||||||
|
input "outlets", "capability.switch", multiple:true
|
||||||
|
}
|
||||||
|
section("Control these locks...") {
|
||||||
|
input "locks", "capability.lock", multiple:true
|
||||||
|
}
|
||||||
|
section("Control these locks...") {
|
||||||
|
input "temperature_sensors", "capability.temperatureMeasurement"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def installed() {
|
||||||
|
log.debug "Installed with settings: ${settings}"
|
||||||
|
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
def updated() {
|
||||||
|
log.debug "Updated with settings: ${settings}"
|
||||||
|
|
||||||
|
unsubscribe()
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
def initialize() {
|
||||||
|
// TODO: subscribe to attributes, devices, locations, etc.
|
||||||
|
subscribe(outlet, "energy", outletHandler)
|
||||||
|
subscribe(outlet, "switch", outletHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement event handlers
|
||||||
|
def outletHandler(evt) {
|
||||||
|
log.debug "$outlet.currentEnergy"
|
||||||
|
//TODO call G API
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private device(it, type) {
|
||||||
|
it ? [id: it.id, label: it.label, type: type] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
//API Mapping
|
||||||
|
mappings {
|
||||||
|
path("/getalldevices") {
|
||||||
|
action: [
|
||||||
|
GET: "getAllDevices"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
path("/doorlocks/:id/:command") {
|
||||||
|
action: [
|
||||||
|
GET: "updateDoorLock"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
path("/doorlocks/:id") {
|
||||||
|
action: [
|
||||||
|
GET: "getDoorLockStatus"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
path("/tempsensors/:id") {
|
||||||
|
action: [
|
||||||
|
GET: "getTempSensorsStatus"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
path("/presences/:id") {
|
||||||
|
action: [
|
||||||
|
GET: "getPresenceStatus"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
path("/motions/:id") {
|
||||||
|
action: [
|
||||||
|
GET: "getMotionStatus"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
path("/outlets/:id") {
|
||||||
|
action: [
|
||||||
|
GET: "getOutletStatus"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
path("/outlets/:id/:command") {
|
||||||
|
action: [
|
||||||
|
GET: "updateOutlet"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
path("/switches/:command") {
|
||||||
|
action: [
|
||||||
|
PUT: "updateSwitch"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//API Methods
|
||||||
|
def getAllDevices() {
|
||||||
|
def locks_list = locks.collect{device(it,"Lock")}
|
||||||
|
def presences_list = presence_sensors.collect{device(it,"Presence")}
|
||||||
|
def motions_list = motions.collect{device(it,"Motion")}
|
||||||
|
def outlets_list = outlets.collect{device(it,"Outlet")}
|
||||||
|
def switches_list = switches.collect{device(it,"Switch")}
|
||||||
|
def temp_list = temperature_sensors.collect{device(it,"Temperature")}
|
||||||
|
return [Locks: locks_list, Presences: presences_list, Motions: motions_list, Outlets: outlets_list, Switches: switches_list, Temperatures: temp_list]
|
||||||
|
}
|
||||||
|
|
||||||
|
//LOCKS
|
||||||
|
def getDoorLockStatus() {
|
||||||
|
def device = locks.find { it.id == params.id }
|
||||||
|
if (!device) {
|
||||||
|
httpError(404, "Device not found")
|
||||||
|
} else {
|
||||||
|
return [Device_state: device.currentValue('lock')]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def updateDoorLock() {
|
||||||
|
def command = params.command
|
||||||
|
def device = locks.find { it.id == params.id }
|
||||||
|
if (command){
|
||||||
|
if (!device) {
|
||||||
|
httpError(404, "Device not found")
|
||||||
|
} else {
|
||||||
|
if(command == "toggle")
|
||||||
|
{
|
||||||
|
if(device.currentValue('lock') == "locked")
|
||||||
|
device.unlock();
|
||||||
|
else
|
||||||
|
device.lock();
|
||||||
|
|
||||||
|
return [Device_id: params.id, result_action: "200"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//PRESENCE
|
||||||
|
def getPresenceStatus() {
|
||||||
|
|
||||||
|
def device = presence_sensors.find { it.id == params.id }
|
||||||
|
if (!device) {
|
||||||
|
httpError(404, "Device not found")
|
||||||
|
} else {
|
||||||
|
return [Device_state: device.currentValue('presence')]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//MOTION
|
||||||
|
def getMotionStatus() {
|
||||||
|
|
||||||
|
def device = motions.find { it.id == params.id }
|
||||||
|
if (!device) {
|
||||||
|
httpError(404, "Device not found")
|
||||||
|
} else {
|
||||||
|
return [Device_state: device.currentValue('motion')]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//OUTLET
|
||||||
|
def getOutletStatus() {
|
||||||
|
|
||||||
|
def device = outlets.find { it.id == params.id }
|
||||||
|
if (!device) {
|
||||||
|
httpError(404, "Device not found")
|
||||||
|
} else {
|
||||||
|
return [Device_state: device.currentSwitch, Current_watt: device.currentValue("energy")]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def updateOutlet() {
|
||||||
|
|
||||||
|
def command = params.command
|
||||||
|
def device = outlets.find { it.id == params.id }
|
||||||
|
if (command){
|
||||||
|
if (!device) {
|
||||||
|
httpError(404, "Device not found")
|
||||||
|
} else {
|
||||||
|
if(command == "toggle")
|
||||||
|
{
|
||||||
|
if(device.currentSwitch == "on")
|
||||||
|
device.off();
|
||||||
|
else
|
||||||
|
device.on();
|
||||||
|
|
||||||
|
return [Device_id: params.id, result_action: "200"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//SWITCH
|
||||||
|
def updateSwitch() {
|
||||||
|
def command = params.command
|
||||||
|
def device = switches.find { it.id == params.id }
|
||||||
|
if (command){
|
||||||
|
if (!device) {
|
||||||
|
httpError(404, "Device not found")
|
||||||
|
} else {
|
||||||
|
if(command == "toggle")
|
||||||
|
{
|
||||||
|
if(device.currentSwitch == "on")
|
||||||
|
device.off();
|
||||||
|
else
|
||||||
|
device.on();
|
||||||
|
|
||||||
|
return [Device_id: params.id, result_action: "200"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TEMPERATURE
|
||||||
|
def getTempSensorsStatus() {
|
||||||
|
|
||||||
|
def device = temperature_sensors.find { it.id == params.id }
|
||||||
|
if (!device) {
|
||||||
|
httpError(404, "Device not found")
|
||||||
|
} else {
|
||||||
|
return [Device_state: device.currentValue('temperature')]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,8 +18,13 @@ definition(
|
|||||||
)
|
)
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
section("Zip code?") {
|
|
||||||
input "zipcode", "text", title: "Zipcode?"
|
if (!(location.zipCode || ( location.latitude && location.longitude )) && location.channelName == 'samsungtv') {
|
||||||
|
section { paragraph title: "Note:", "Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location." }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location.channelName != 'samsungtv') {
|
||||||
|
section( "Set your location" ) { input "zipCode", "text", title: "Zip code" }
|
||||||
}
|
}
|
||||||
|
|
||||||
section("Things to check?") {
|
section("Things to check?") {
|
||||||
@@ -60,7 +65,11 @@ def scheduleCheck(evt) {
|
|||||||
// Only need to poll if we haven't checked in a while - and if something is left open.
|
// Only need to poll if we haven't checked in a while - and if something is left open.
|
||||||
if((now() - (30 * 60 * 1000) > state.lastCheck["time"]) && open) {
|
if((now() - (30 * 60 * 1000) > state.lastCheck["time"]) && open) {
|
||||||
log.info("Something's open - let's check the weather.")
|
log.info("Something's open - let's check the weather.")
|
||||||
def response = getWeatherFeature("forecast", zipcode)
|
def response
|
||||||
|
if (location.channelName != 'samsungtv')
|
||||||
|
response = getWeatherFeature("forecast", zipCode)
|
||||||
|
else
|
||||||
|
response = getWeatherFeature("forecast")
|
||||||
def weather = isStormy(response)
|
def weather = isStormy(response)
|
||||||
|
|
||||||
if(weather) {
|
if(weather) {
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ preferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
section("Temperature monitor?") {
|
section("Temperature monitor?") {
|
||||||
input "temp", "capability.temperatureMeasurement", title: "Temp Sensor", required: false
|
input "temp", "capability.temperatureMeasurement", title: "Temperature Sensor", required: false
|
||||||
input "maxTemp", "number", title: "Max Temp?", required: false
|
input "maxTemp", "number", title: "Max Temperature (°${location.temperatureScale})", required: false
|
||||||
input "minTemp", "number", title: "Min Temp?", required: false
|
input "minTemp", "number", title: "Min Temperature (°${location.temperatureScale})", required: false
|
||||||
}
|
}
|
||||||
|
|
||||||
section("When which people are away?") {
|
section("When which people are away?") {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
* Author: Juan Risso
|
* Author: Juan Risso
|
||||||
* Date: 2013-12-19
|
* Date: 2013-12-19
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
include 'asynchttp_v1'
|
||||||
|
|
||||||
definition(
|
definition(
|
||||||
name: "Jawbone UP (Connect)",
|
name: "Jawbone UP (Connect)",
|
||||||
namespace: "juano2310",
|
namespace: "juano2310",
|
||||||
@@ -28,7 +31,7 @@ mappings {
|
|||||||
path("/receivedToken") { action: [ POST: "receivedToken", GET: "receivedToken"] }
|
path("/receivedToken") { action: [ POST: "receivedToken", GET: "receivedToken"] }
|
||||||
path("/receiveToken") { action: [ POST: "receiveToken", GET: "receiveToken"] }
|
path("/receiveToken") { action: [ POST: "receiveToken", GET: "receiveToken"] }
|
||||||
path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] }
|
path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] }
|
||||||
path("/oauth/initialize") {action: [GET: "oauthInitUrl"]}
|
path("/oauth/initialize") {action: [GET: "oauthInitUrl"]}
|
||||||
path("/oauth/callback") { action: [ GET: "callback" ] }
|
path("/oauth/callback") { action: [ GET: "callback" ] }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +47,7 @@ def callback() {
|
|||||||
} else {
|
} else {
|
||||||
log.warn "No authQueryString"
|
log.warn "No authQueryString"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.JawboneAccessToken) {
|
if (state.JawboneAccessToken) {
|
||||||
log.debug "Access token already exists"
|
log.debug "Access token already exists"
|
||||||
setup()
|
setup()
|
||||||
@@ -73,7 +76,7 @@ def callback() {
|
|||||||
|
|
||||||
def authPage() {
|
def authPage() {
|
||||||
log.debug "authPage"
|
log.debug "authPage"
|
||||||
def description = null
|
def description = null
|
||||||
if (state.JawboneAccessToken == null) {
|
if (state.JawboneAccessToken == null) {
|
||||||
if (!state.accessToken) {
|
if (!state.accessToken) {
|
||||||
log.debug "About to create access token"
|
log.debug "About to create access token"
|
||||||
@@ -82,12 +85,13 @@ def authPage() {
|
|||||||
description = "Click to enter Jawbone Credentials"
|
description = "Click to enter Jawbone Credentials"
|
||||||
def redirectUrl = buildRedirectUrl
|
def redirectUrl = buildRedirectUrl
|
||||||
log.debug "RedirectURL = ${redirectUrl}"
|
log.debug "RedirectURL = ${redirectUrl}"
|
||||||
def donebutton= state.JawboneAccessToken != null
|
def donebutton= state.JawboneAccessToken != null
|
||||||
return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install: donebutton) {
|
return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install: donebutton) {
|
||||||
|
section { paragraph title: "Note:", "This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance." }
|
||||||
section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", state: hast ,description:description }
|
section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", state: hast ,description:description }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
description = "Jawbone Credentials Already Entered."
|
description = "Jawbone Credentials Already Entered."
|
||||||
return dynamicPage(name: "Credentials", title: "Jawbone UP", uninstall: true, install:true) {
|
return dynamicPage(name: "Credentials", title: "Jawbone UP", uninstall: true, install:true) {
|
||||||
section { href url: buildRedirectUrl("receivedToken"), style:"embedded", state: "complete", title:"Jawbone UP", description:description }
|
section { href url: buildRedirectUrl("receivedToken"), style:"embedded", state: "complete", title:"Jawbone UP", description:description }
|
||||||
}
|
}
|
||||||
@@ -107,7 +111,7 @@ def receiveToken(redirectUrl = null) {
|
|||||||
def params = [
|
def params = [
|
||||||
uri: "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}",
|
uri: "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}",
|
||||||
]
|
]
|
||||||
httpGet(params) { response ->
|
httpGet(params) { response ->
|
||||||
log.debug "${response.data}"
|
log.debug "${response.data}"
|
||||||
log.debug "Setting access token to ${response.data.access_token}, refresh token to ${response.data.refresh_token}"
|
log.debug "Setting access token to ${response.data.access_token}, refresh token to ${response.data.refresh_token}"
|
||||||
state.JawboneAccessToken = response.data.access_token
|
state.JawboneAccessToken = response.data.access_token
|
||||||
@@ -149,7 +153,7 @@ def connectionStatus(message, redirectUrl = null) {
|
|||||||
<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
|
<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
def html = """
|
def html = """
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
@@ -229,12 +233,12 @@ def validateCurrentToken() {
|
|||||||
log.debug "validateCurrentToken"
|
log.debug "validateCurrentToken"
|
||||||
def url = "https://jawbone.com/nudge/api/v.1.1/users/@me/refreshToken"
|
def url = "https://jawbone.com/nudge/api/v.1.1/users/@me/refreshToken"
|
||||||
def requestBody = "secret=${appSettings.clientSecret}"
|
def requestBody = "secret=${appSettings.clientSecret}"
|
||||||
|
|
||||||
try {
|
try {
|
||||||
httpPost(uri: url, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], body: requestBody) {response ->
|
httpPost(uri: url, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], body: requestBody) {response ->
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
log.debug "${response.data}"
|
log.debug "${response.data}"
|
||||||
log.debug "Setting refresh token to ${response.data.data.refresh_token}"
|
log.debug "Setting refresh token"
|
||||||
state.refreshToken = response.data.data.refresh_token
|
state.refreshToken = response.data.data.refresh_token
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,7 +262,7 @@ def validateCurrentToken() {
|
|||||||
state.remove("refreshToken")
|
state.remove("refreshToken")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.debug "Setting access token to ${data.access_token}, refresh token to ${data.refresh_token}"
|
log.debug "Setting access token"
|
||||||
state.JawboneAccessToken = data.access_token
|
state.JawboneAccessToken = data.access_token
|
||||||
state.refreshToken = data.refresh_token
|
state.refreshToken = data.refresh_token
|
||||||
}
|
}
|
||||||
@@ -271,10 +275,10 @@ def validateCurrentToken() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def initialize() {
|
def initialize() {
|
||||||
log.debug "Callback URL - Webhook"
|
log.debug "Callback URL - Webhook"
|
||||||
def localServerUrl = getApiServerUrl()
|
def localServerUrl = getApiServerUrl()
|
||||||
def hookUrl = "${localServerUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback"
|
def hookUrl = "${localServerUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback"
|
||||||
def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl"
|
def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl"
|
||||||
httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ])
|
httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,16 +288,16 @@ def setup() {
|
|||||||
|
|
||||||
if (state.JawboneAccessToken) {
|
if (state.JawboneAccessToken) {
|
||||||
def urlmember = "https://jawbone.com/nudge/api/users/@me/"
|
def urlmember = "https://jawbone.com/nudge/api/users/@me/"
|
||||||
def member = null
|
def member = null
|
||||||
httpGet(uri: urlmember, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
httpGet(uri: urlmember, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||||
member = response.data.data
|
member = response.data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
if (member) {
|
if (member) {
|
||||||
state.member = member
|
state.member = member
|
||||||
def externalId = "${app.id}.${member.xid}"
|
def externalId = "${app.id}.${member.xid}"
|
||||||
|
|
||||||
// find the appropriate child device based on my app id and the device network id
|
// find the appropriate child device based on my app id and the device network id
|
||||||
def deviceWrapper = getChildDevice("${externalId}")
|
def deviceWrapper = getChildDevice("${externalId}")
|
||||||
|
|
||||||
// invoke the generatePresenceEvent method on the child device
|
// invoke the generatePresenceEvent method on the child device
|
||||||
@@ -302,7 +306,8 @@ def setup() {
|
|||||||
def childDevice = addChildDevice('juano2310', "Jawbone User", "${app.id}.${member.xid}",null,[name:"Jawbone UP - " + member.first, completedSetup: true])
|
def childDevice = addChildDevice('juano2310', "Jawbone User", "${app.id}.${member.xid}",null,[name:"Jawbone UP - " + member.first, completedSetup: true])
|
||||||
if (childDevice) {
|
if (childDevice) {
|
||||||
log.debug "Child Device Successfully Created"
|
log.debug "Child Device Successfully Created"
|
||||||
generateInitialEvent (member, childDevice)
|
childDevice?.generateSleepingEvent(false)
|
||||||
|
pollChild(childDevice)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -312,7 +317,7 @@ def setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def installed() {
|
def installed() {
|
||||||
|
|
||||||
if (!state.accessToken) {
|
if (!state.accessToken) {
|
||||||
log.debug "About to create access token"
|
log.debug "About to create access token"
|
||||||
createAccessToken()
|
createAccessToken()
|
||||||
@@ -324,7 +329,7 @@ def installed() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def updated() {
|
def updated() {
|
||||||
|
|
||||||
if (!state.accessToken) {
|
if (!state.accessToken) {
|
||||||
log.debug "About to create access token"
|
log.debug "About to create access token"
|
||||||
createAccessToken()
|
createAccessToken()
|
||||||
@@ -348,128 +353,128 @@ def uninstalled() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def pollChild(childDevice) {
|
def pollChild(childDevice) {
|
||||||
def member = state.member
|
def childMap = [ value: "$childDevice.device.deviceNetworkId}"]
|
||||||
generatePollingEvents (member, childDevice)
|
|
||||||
|
def params = [
|
||||||
|
uri: 'https://jawbone.com',
|
||||||
|
path: '/nudge/api/users/@me/goals',
|
||||||
|
headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ],
|
||||||
|
contentType: 'application/json'
|
||||||
|
]
|
||||||
|
|
||||||
|
asynchttp_v1.get('responseGoals', params, childMap)
|
||||||
|
|
||||||
|
def params2 = [
|
||||||
|
uri: 'https://jawbone.com',
|
||||||
|
path: '/nudge/api/users/@me/moves',
|
||||||
|
headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ],
|
||||||
|
contentType: 'application/json'
|
||||||
|
]
|
||||||
|
|
||||||
|
asynchttp_v1.get('responseMoves', params2, childMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
def generatePollingEvents (member, childDevice) {
|
def responseGoals(response, dni) {
|
||||||
// lets figure out if the member is currently "home" (At the place)
|
if (response.hasError()) {
|
||||||
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
|
log.error "response has error: $response.errorMessage"
|
||||||
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
|
} else {
|
||||||
def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps"
|
def goals
|
||||||
def goals = null
|
try {
|
||||||
def moves = null
|
// json response already parsed into JSONElement object
|
||||||
def sleeps = null
|
goals = response.json.data
|
||||||
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
} catch (e) {
|
||||||
goals = response.data.data
|
log.error "error parsing json from response: $e"
|
||||||
}
|
}
|
||||||
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
if (goals) {
|
||||||
moves = response.data.data.items[0]
|
def childDevice = getChildDevice(dni.value)
|
||||||
}
|
log.debug "Goal = ${goals.move_steps} Steps"
|
||||||
|
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
|
||||||
try { // we are going to just ignore any errors
|
} else {
|
||||||
log.debug "Member = ${member.first}"
|
log.debug "did not get json results from response body: $response.data"
|
||||||
log.debug "Moves Goal = ${goals.move_steps} Steps"
|
}
|
||||||
log.debug "Moves = ${moves.details.steps} Steps"
|
}
|
||||||
|
|
||||||
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
|
|
||||||
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
|
|
||||||
//setColor(moves.details.steps,goals.move_steps,childDevice)
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
// eat it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def generateInitialEvent (member, childDevice) {
|
def responseMoves(response, dni) {
|
||||||
// lets figure out if the member is currently "home" (At the place)
|
if (response.hasError()) {
|
||||||
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
|
log.error "response has error: $response.errorMessage"
|
||||||
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
|
} else {
|
||||||
def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps"
|
def moves
|
||||||
def goals = null
|
try {
|
||||||
def moves = null
|
// json response already parsed into JSONElement object
|
||||||
def sleeps = null
|
moves = response.json.data.items[0]
|
||||||
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
} catch (e) {
|
||||||
goals = response.data.data
|
log.error "error parsing json from response: $e"
|
||||||
}
|
}
|
||||||
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
if (moves) {
|
||||||
moves = response.data.data.items[0]
|
def childDevice = getChildDevice(dni.value)
|
||||||
}
|
log.debug "Moves = ${moves.details.steps} Steps"
|
||||||
|
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
|
||||||
try { // we are going to just ignore any errors
|
} else {
|
||||||
log.debug "Member = ${member.first}"
|
log.debug "did not get json results from response body: $response.data"
|
||||||
log.debug "Moves Goal = ${goals.move_steps} Steps"
|
}
|
||||||
log.debug "Moves = ${moves.details.steps} Steps"
|
}
|
||||||
log.debug "Sleeping state = false"
|
|
||||||
childDevice?.generateSleepingEvent(false)
|
|
||||||
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
|
|
||||||
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
|
|
||||||
//setColor(moves.details.steps,goals.move_steps,childDevice)
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
// eat it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def setColor (steps,goal,childDevice) {
|
def setColor (steps,goal,childDevice) {
|
||||||
def result = steps * 100 / goal
|
def result = steps * 100 / goal
|
||||||
if (result < 25)
|
if (result < 25)
|
||||||
childDevice?.sendEvent(name:"steps", value: "steps", label: steps)
|
childDevice?.sendEvent(name:"steps", value: "steps", label: steps)
|
||||||
else if ((result >= 25) && (result < 50))
|
else if ((result >= 25) && (result < 50))
|
||||||
childDevice?.sendEvent(name:"steps", value: "steps1", label: steps)
|
childDevice?.sendEvent(name:"steps", value: "steps1", label: steps)
|
||||||
else if ((result >= 50) && (result < 75))
|
else if ((result >= 50) && (result < 75))
|
||||||
childDevice?.sendEvent(name:"steps", value: "steps1", label: steps)
|
childDevice?.sendEvent(name:"steps", value: "steps1", label: steps)
|
||||||
else if (result >= 75)
|
else if (result >= 75)
|
||||||
childDevice?.sendEvent(name:"steps", value: "stepsgoal", label: steps)
|
childDevice?.sendEvent(name:"steps", value: "stepsgoal", label: steps)
|
||||||
}
|
}
|
||||||
|
|
||||||
def hookEventHandler() {
|
def hookEventHandler() {
|
||||||
// log.debug "In hookEventHandler method."
|
// log.debug "In hookEventHandler method."
|
||||||
log.debug "request = ${request}"
|
log.debug "request = ${request}"
|
||||||
|
|
||||||
def json = request.JSON
|
def json = request.JSON
|
||||||
|
|
||||||
// get some stuff we need
|
// get some stuff we need
|
||||||
def userId = json.events.user_xid[0]
|
def userId = json.events.user_xid[0]
|
||||||
def json_type = json.events.type[0]
|
def json_type = json.events.type[0]
|
||||||
def json_action = json.events.action[0]
|
def json_action = json.events.action[0]
|
||||||
|
|
||||||
//log.debug json
|
//log.debug json
|
||||||
log.debug "Userid = ${userId}"
|
log.debug "Userid = ${userId}"
|
||||||
log.debug "Notification Type: " + json_type
|
log.debug "Notification Type: " + json_type
|
||||||
log.debug "Notification Action: " + json_action
|
log.debug "Notification Action: " + json_action
|
||||||
|
|
||||||
// find the appropriate child device based on my app id and the device network id
|
// find the appropriate child device based on my app id and the device network id
|
||||||
def externalId = "${app.id}.${userId}"
|
def externalId = "${app.id}.${userId}"
|
||||||
def childDevice = getChildDevice("${externalId}")
|
def childDevice = getChildDevice("${externalId}")
|
||||||
|
|
||||||
if (childDevice) {
|
if (childDevice) {
|
||||||
switch (json_action) {
|
switch (json_action) {
|
||||||
case "enter_sleep_mode":
|
case "enter_sleep_mode":
|
||||||
childDevice?.generateSleepingEvent(true)
|
childDevice?.generateSleepingEvent(true)
|
||||||
break
|
break
|
||||||
case "exit_sleep_mode":
|
case "exit_sleep_mode":
|
||||||
childDevice?.generateSleepingEvent(false)
|
childDevice?.generateSleepingEvent(false)
|
||||||
break
|
break
|
||||||
case "creation":
|
case "creation":
|
||||||
childDevice?.sendEvent(name:"steps", value: 0)
|
childDevice?.sendEvent(name:"steps", value: 0)
|
||||||
break
|
break
|
||||||
case "updation":
|
case "updation":
|
||||||
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
|
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
|
||||||
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
|
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
|
||||||
def goals = null
|
def goals = null
|
||||||
def moves = null
|
def moves = null
|
||||||
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||||
goals = response.data.data
|
goals = response.data.data
|
||||||
}
|
}
|
||||||
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||||
moves = response.data.data.items[0]
|
moves = response.data.data.items[0]
|
||||||
}
|
}
|
||||||
log.debug "Goal = ${goals.move_steps} Steps"
|
log.debug "Goal = ${goals.move_steps} Steps"
|
||||||
log.debug "Steps = ${moves.details.steps} Steps"
|
log.debug "Steps = ${moves.details.steps} Steps"
|
||||||
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
|
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
|
||||||
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
|
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
|
||||||
//setColor(moves.details.steps,goals.move_steps,childDevice)
|
//setColor(moves.details.steps,goals.move_steps,childDevice)
|
||||||
break
|
break
|
||||||
case "deletion":
|
case "deletion":
|
||||||
app.delete()
|
app.delete()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
* for the specific language governing permissions and limitations under the License.
|
* for the specific language governing permissions and limitations under the License.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
definition(
|
definition(
|
||||||
name: "Smart Home Ventilation",
|
name: "Smart Home Ventilation",
|
||||||
namespace: "MichaelStruck",
|
namespace: "MichaelStruck",
|
||||||
@@ -164,7 +164,7 @@ def installed() {
|
|||||||
def updated() {
|
def updated() {
|
||||||
unschedule()
|
unschedule()
|
||||||
turnOffSwitch() //Turn off all switches if the schedules are changed while in mid-schedule
|
turnOffSwitch() //Turn off all switches if the schedules are changed while in mid-schedule
|
||||||
unsubscribe
|
unsubscribe()
|
||||||
log.debug "Updated with settings: ${settings}"
|
log.debug "Updated with settings: ${settings}"
|
||||||
init()
|
init()
|
||||||
}
|
}
|
||||||
@@ -174,12 +174,12 @@ def init() {
|
|||||||
schedule (midnightTime, midNight)
|
schedule (midnightTime, midNight)
|
||||||
subscribe(location, "mode", locationHandler)
|
subscribe(location, "mode", locationHandler)
|
||||||
startProcess()
|
startProcess()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common methods
|
// Common methods
|
||||||
|
|
||||||
def startProcess () {
|
def startProcess () {
|
||||||
createDayArray()
|
createDayArray()
|
||||||
state.dayCount=state.data.size()
|
state.dayCount=state.data.size()
|
||||||
if (state.dayCount){
|
if (state.dayCount){
|
||||||
state.counter = 0
|
state.counter = 0
|
||||||
@@ -190,7 +190,7 @@ def startProcess () {
|
|||||||
def startDay() {
|
def startDay() {
|
||||||
def start = convertEpoch(state.data[state.counter].start)
|
def start = convertEpoch(state.data[state.counter].start)
|
||||||
def stop = convertEpoch(state.data[state.counter].stop)
|
def stop = convertEpoch(state.data[state.counter].stop)
|
||||||
|
|
||||||
runOnce(start, turnOnSwitch, [overwrite: true])
|
runOnce(start, turnOnSwitch, [overwrite: true])
|
||||||
runOnce(stop, incDay, [overwrite: true])
|
runOnce(stop, incDay, [overwrite: true])
|
||||||
}
|
}
|
||||||
@@ -218,7 +218,7 @@ def locationHandler(evt) {
|
|||||||
}
|
}
|
||||||
if (!result) {
|
if (!result) {
|
||||||
startProcess()
|
startProcess()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def midNight(){
|
def midNight(){
|
||||||
@@ -238,7 +238,7 @@ def turnOffSwitch() {
|
|||||||
}
|
}
|
||||||
log.debug "Home ventilation switches are off."
|
log.debug "Home ventilation switches are off."
|
||||||
}
|
}
|
||||||
|
|
||||||
def schedDesc(on1, off1, on2, off2, on3, off3, on4, off4, modeList, dayList) {
|
def schedDesc(on1, off1, on2, off2, on3, off3, on4, off4, modeList, dayList) {
|
||||||
def title = ""
|
def title = ""
|
||||||
def dayListClean = "On "
|
def dayListClean = "On "
|
||||||
@@ -252,7 +252,7 @@ def schedDesc(on1, off1, on2, off2, on3, off3, on4, off4, modeList, dayList) {
|
|||||||
dayListClean = "${dayListClean}, "
|
dayListClean = "${dayListClean}, "
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
dayListClean = "Every day"
|
dayListClean = "Every day"
|
||||||
}
|
}
|
||||||
@@ -272,7 +272,7 @@ def schedDesc(on1, off1, on2, off2, on3, off3, on4, off4, modeList, dayList) {
|
|||||||
modeListClean = "${modeListClean} ${modePrefix}"
|
modeListClean = "${modeListClean} ${modePrefix}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
modeListClean = "${modeListClean}all modes"
|
modeListClean = "${modeListClean}all modes"
|
||||||
}
|
}
|
||||||
@@ -283,16 +283,16 @@ def schedDesc(on1, off1, on2, off2, on3, off3, on4, off4, modeList, dayList) {
|
|||||||
title += "\nSchedule 2: ${humanReadableTime(on2)} to ${humanReadableTime(off2)}"
|
title += "\nSchedule 2: ${humanReadableTime(on2)} to ${humanReadableTime(off2)}"
|
||||||
}
|
}
|
||||||
if (on3 && off3) {
|
if (on3 && off3) {
|
||||||
title += "\nSchedule 3: ${humanReadableTime(on3)} to ${humanReadableTime(off3)}"
|
title += "\nSchedule 3: ${humanReadableTime(on3)} to ${humanReadableTime(off3)}"
|
||||||
}
|
}
|
||||||
if (on4 && off4) {
|
if (on4 && off4) {
|
||||||
title += "\nSchedule 4: ${humanReadableTime(on4)} to ${humanReadableTime(off4)}"
|
title += "\nSchedule 4: ${humanReadableTime(on4)} to ${humanReadableTime(off4)}"
|
||||||
}
|
}
|
||||||
if (on1 || on2 || on3 || on4) {
|
if (on1 || on2 || on3 || on4) {
|
||||||
title += "\n$modeListClean"
|
title += "\n$modeListClean"
|
||||||
title += "\n$dayListClean"
|
title += "\n$dayListClean"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!on1 && !on2 && !on3 && !on4) {
|
if (!on1 && !on2 && !on3 && !on4) {
|
||||||
title="Click to configure scenario"
|
title="Click to configure scenario"
|
||||||
}
|
}
|
||||||
@@ -374,7 +374,7 @@ def createDayArray() {
|
|||||||
timeOk(timeOnD1, timeOffD1)
|
timeOk(timeOnD1, timeOffD1)
|
||||||
timeOk(timeOnD2, timeOffD2)
|
timeOk(timeOnD2, timeOffD2)
|
||||||
timeOk(timeOnD3, timeOffD3)
|
timeOk(timeOnD3, timeOffD3)
|
||||||
timeOk(timeOnD4, timeOffD4)
|
timeOk(timeOnD4, timeOffD4)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.data.sort{it.start}
|
state.data.sort{it.start}
|
||||||
@@ -384,7 +384,7 @@ def createDayArray() {
|
|||||||
|
|
||||||
private def textAppName() {
|
private def textAppName() {
|
||||||
def text = "Smart Home Ventilation"
|
def text = "Smart Home Ventilation"
|
||||||
}
|
}
|
||||||
|
|
||||||
private def textVersion() {
|
private def textVersion() {
|
||||||
def text = "Version 2.1.2 (05/31/2015)"
|
def text = "Version 2.1.2 (05/31/2015)"
|
||||||
@@ -416,4 +416,4 @@ private def textHelp() {
|
|||||||
"that each scenario does not overlap and run in separate modes (i.e. Home, Out of town, etc). Also note that you should " +
|
"that each scenario does not overlap and run in separate modes (i.e. Home, Out of town, etc). Also note that you should " +
|
||||||
"avoid scheduling the ventilation fan at exactly midnight; the app resets itself at that time. It is suggested to start any new schedule " +
|
"avoid scheduling the ventilation fan at exactly midnight; the app resets itself at that time. It is suggested to start any new schedule " +
|
||||||
"at 12:15 am or later."
|
"at 12:15 am or later."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -114,13 +114,16 @@ def beaconHandler(evt) {
|
|||||||
|
|
||||||
if (allOk) {
|
if (allOk) {
|
||||||
def data = new groovy.json.JsonSlurper().parseText(evt.data)
|
def data = new groovy.json.JsonSlurper().parseText(evt.data)
|
||||||
log.debug "<beacon-control> data: $data - phones: " + phones*.deviceNetworkId
|
// removed logging of device names. can be added back for debugging
|
||||||
|
//log.debug "<beacon-control> data: $data - phones: " + phones*.deviceNetworkId
|
||||||
|
|
||||||
def beaconName = getBeaconName(evt)
|
def beaconName = getBeaconName(evt)
|
||||||
log.debug "<beacon-control> beaconName: $beaconName"
|
// removed logging of device names. can be added back for debugging
|
||||||
|
//log.debug "<beacon-control> beaconName: $beaconName"
|
||||||
|
|
||||||
def phoneName = getPhoneName(data)
|
def phoneName = getPhoneName(data)
|
||||||
log.debug "<beacon-control> phoneName: $phoneName"
|
// removed logging of device names. can be added back for debugging
|
||||||
|
//log.debug "<beacon-control> phoneName: $phoneName"
|
||||||
if (phoneName != null) {
|
if (phoneName != null) {
|
||||||
def action = data.presence == "1" ? "arrived" : "left"
|
def action = data.presence == "1" ? "arrived" : "left"
|
||||||
def msg = "$phoneName has $action ${action == 'arrived' ? 'at ' : ''}the $beaconName"
|
def msg = "$phoneName has $action ${action == 'arrived' ? 'at ' : ''}the $beaconName"
|
||||||
|
|||||||
@@ -49,13 +49,15 @@ preferences {
|
|||||||
|
|
||||||
def installed() {
|
def installed() {
|
||||||
log.debug "Installed with settings: ${settings}"
|
log.debug "Installed with settings: ${settings}"
|
||||||
log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
// commented out log statement because presence sensor label could contain user's name
|
||||||
|
//log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
||||||
subscribe(people, "presence", presence)
|
subscribe(people, "presence", presence)
|
||||||
}
|
}
|
||||||
|
|
||||||
def updated() {
|
def updated() {
|
||||||
log.debug "Updated with settings: ${settings}"
|
log.debug "Updated with settings: ${settings}"
|
||||||
log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
// commented out log statement because presence sensor label could contain user's name
|
||||||
|
//log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
||||||
unsubscribe()
|
unsubscribe()
|
||||||
subscribe(people, "presence", presence)
|
subscribe(people, "presence", presence)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -54,10 +54,10 @@ def waterWetHandler(evt) {
|
|||||||
def alreadySentSms = recentEvents.count { it.value && it.value == "wet" } > 1
|
def alreadySentSms = recentEvents.count { it.value && it.value == "wet" } > 1
|
||||||
|
|
||||||
if (alreadySentSms) {
|
if (alreadySentSms) {
|
||||||
log.debug "SMS already sent to $phone within the last $deltaSeconds seconds"
|
log.debug "SMS already sent within the last $deltaSeconds seconds"
|
||||||
} else {
|
} else {
|
||||||
def msg = "${alarm.displayName} is wet!"
|
def msg = "${alarm.displayName} is wet!"
|
||||||
log.debug "$alarm is wet, texting $phone"
|
log.debug "$alarm is wet, texting phone number"
|
||||||
|
|
||||||
if (location.contactBookEnabled) {
|
if (location.contactBookEnabled) {
|
||||||
sendNotificationToContacts(msg, recipients)
|
sendNotificationToContacts(msg, recipients)
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ def takeAction(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
def sendTextMessage() {
|
def sendTextMessage() {
|
||||||
log.debug "$multisensor was open too long, texting $phone"
|
log.debug "$multisensor was open too long, texting phone"
|
||||||
|
|
||||||
updateSmsHistory()
|
updateSmsHistory()
|
||||||
def openMinutes = maxOpenTime * (state.smsHistory?.size() ?: 1)
|
def openMinutes = maxOpenTime * (state.smsHistory?.size() ?: 1)
|
||||||
|
|||||||
@@ -454,17 +454,23 @@ def sendStopEvent(source) {
|
|||||||
eventData.value += "cancelled"
|
eventData.value += "cancelled"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// send 100% completion event
|
||||||
|
sendTimeRemainingEvent(100)
|
||||||
|
|
||||||
|
// send a non-displayed 0% completion to reset tiles
|
||||||
|
sendTimeRemainingEvent(0, false)
|
||||||
|
|
||||||
|
// send sessionStatus event last so the event feed is ordered properly
|
||||||
sendControllerEvent(eventData)
|
sendControllerEvent(eventData)
|
||||||
sendTimeRemainingEvent(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def sendTimeRemainingEvent(percentComplete) {
|
def sendTimeRemainingEvent(percentComplete, displayed = true) {
|
||||||
log.trace "sendTimeRemainingEvent(${percentComplete})"
|
log.trace "sendTimeRemainingEvent(${percentComplete})"
|
||||||
|
|
||||||
def percentCompleteEventData = [
|
def percentCompleteEventData = [
|
||||||
name: "percentComplete",
|
name: "percentComplete",
|
||||||
value: percentComplete as int,
|
value: percentComplete as int,
|
||||||
displayed: true,
|
displayed: displayed,
|
||||||
isStateChange: true
|
isStateChange: true
|
||||||
]
|
]
|
||||||
sendControllerEvent(percentCompleteEventData)
|
sendControllerEvent(percentCompleteEventData)
|
||||||
@@ -474,7 +480,7 @@ def sendTimeRemainingEvent(percentComplete) {
|
|||||||
def timeRemainingEventData = [
|
def timeRemainingEventData = [
|
||||||
name: "timeRemaining",
|
name: "timeRemaining",
|
||||||
value: displayableTime(timeRemaining),
|
value: displayableTime(timeRemaining),
|
||||||
displayed: true,
|
displayed: displayed,
|
||||||
isStateChange: true
|
isStateChange: true
|
||||||
]
|
]
|
||||||
sendControllerEvent(timeRemainingEventData)
|
sendControllerEvent(timeRemainingEventData)
|
||||||
@@ -608,8 +614,6 @@ private completion() {
|
|||||||
handleCompletionMessaging()
|
handleCompletionMessaging()
|
||||||
|
|
||||||
handleCompletionModesAndPhrases()
|
handleCompletionModesAndPhrases()
|
||||||
|
|
||||||
sendTimeRemainingEvent(100)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleCompletionSwitches() {
|
private handleCompletionSwitches() {
|
||||||
@@ -761,7 +765,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 +1105,4 @@ def hasStartLevel() {
|
|||||||
|
|
||||||
def hasEndLevel() {
|
def hasEndLevel() {
|
||||||
return (endLevel != null && endLevel != "")
|
return (endLevel != null && endLevel != "")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,13 +47,13 @@ preferences {
|
|||||||
|
|
||||||
def installed() {
|
def installed() {
|
||||||
log.debug "Installed with settings: ${settings}"
|
log.debug "Installed with settings: ${settings}"
|
||||||
log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
// log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
||||||
subscribe(people, "presence", presence)
|
subscribe(people, "presence", presence)
|
||||||
}
|
}
|
||||||
|
|
||||||
def updated() {
|
def updated() {
|
||||||
log.debug "Updated with settings: ${settings}"
|
log.debug "Updated with settings: ${settings}"
|
||||||
log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
// log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
||||||
unsubscribe()
|
unsubscribe()
|
||||||
subscribe(people, "presence", presence)
|
subscribe(people, "presence", presence)
|
||||||
}
|
}
|
||||||
@@ -71,11 +71,10 @@ def presence(evt)
|
|||||||
def person = getPerson(evt)
|
def person = getPerson(evt)
|
||||||
def recentNotPresent = person.statesSince("presence", t0).find{it.value == "not present"}
|
def recentNotPresent = person.statesSince("presence", t0).find{it.value == "not present"}
|
||||||
if (recentNotPresent) {
|
if (recentNotPresent) {
|
||||||
log.debug "skipping notification of arrival of ${person.displayName} because last departure was only ${now() - recentNotPresent.date.time} msec ago"
|
log.debug "skipping notification of arrival of Person because last departure was only ${now() - recentNotPresent.date.time} msec ago"
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
def message = "${person.displayName} arrived at home, changing mode to '${newMode}'"
|
def message = "${person.displayName} arrived at home, changing mode to '${newMode}'"
|
||||||
log.info message
|
|
||||||
send(message)
|
send(message)
|
||||||
setLocationMode(newMode)
|
setLocationMode(newMode)
|
||||||
}
|
}
|
||||||
@@ -106,6 +105,4 @@ private send(msg) {
|
|||||||
sendSms(phone, msg)
|
sendSms(phone, msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug msg
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,12 +57,11 @@ def scheduleCheck()
|
|||||||
def message = message1 ?: "SmartThings - Habit Helper Reminder!"
|
def message = message1 ?: "SmartThings - Habit Helper Reminder!"
|
||||||
|
|
||||||
if (location.contactBookEnabled) {
|
if (location.contactBookEnabled) {
|
||||||
log.debug "Texting reminder: ($message) to contacts:${recipients?.size()}"
|
log.debug "Texting reminder to contacts:${recipients?.size()}"
|
||||||
sendNotificationToContacts(message, recipients)
|
sendNotificationToContacts(message, recipients)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
log.debug "Texting reminder"
|
||||||
log.debug "Texting reminder: ($message) to $phone1"
|
|
||||||
sendSms(phone1, message)
|
sendSms(phone1, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ def bridgeDiscovery(params=[:])
|
|||||||
|
|
||||||
return dynamicPage(name:"bridgeDiscovery", title:"Discovery Started!", nextPage:"bridgeBtnPush", refreshInterval:refreshInterval, uninstall: true) {
|
return dynamicPage(name:"bridgeDiscovery", title:"Discovery Started!", nextPage:"bridgeBtnPush", refreshInterval:refreshInterval, uninstall: true) {
|
||||||
section("Please wait while we discover your Hue Bridge. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
section("Please wait while we discover your Hue Bridge. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||||
input "selectedHue", "enum", required:false, title:"Select Hue Bridge (${numFound} found)", multiple:false, options:options
|
input "selectedHue", "enum", required:false, title:"Select Hue Bridge (${numFound} found)", multiple:false, options:options, submitOnChange: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,8 +95,7 @@ def bridgeDiscoveryFailed() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def bridgeLinking()
|
def bridgeLinking() {
|
||||||
{
|
|
||||||
int linkRefreshcount = !state.linkRefreshcount ? 0 : state.linkRefreshcount as int
|
int linkRefreshcount = !state.linkRefreshcount ? 0 : state.linkRefreshcount as int
|
||||||
state.linkRefreshcount = linkRefreshcount + 1
|
state.linkRefreshcount = linkRefreshcount + 1
|
||||||
def refreshInterval = 3
|
def refreshInterval = 3
|
||||||
@@ -132,10 +131,7 @@ def bulbDiscovery() {
|
|||||||
def refreshInterval = 3
|
def refreshInterval = 3
|
||||||
state.inBulbDiscovery = true
|
state.inBulbDiscovery = true
|
||||||
def bridge = null
|
def bridge = null
|
||||||
if (selectedHue) {
|
|
||||||
bridge = getChildDevice(selectedHue)
|
|
||||||
subscribe(bridge, "bulbList", bulbListData)
|
|
||||||
}
|
|
||||||
state.bridgeRefreshCount = 0
|
state.bridgeRefreshCount = 0
|
||||||
def allLightsFound = bulbsDiscovered() ?: [:]
|
def allLightsFound = bulbsDiscovered() ?: [:]
|
||||||
|
|
||||||
@@ -171,7 +167,7 @@ def bulbDiscovery() {
|
|||||||
|
|
||||||
return dynamicPage(name:"bulbDiscovery", title:"Light Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
return dynamicPage(name:"bulbDiscovery", title:"Light Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
||||||
section("Please wait while we discover your Hue Lights. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
section("Please wait while we discover your Hue Lights. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||||
input "selectedBulbs", "enum", required:false, title:"Select Hue Lights to add (${numFound} found)", multiple:true, options:newLights
|
input "selectedBulbs", "enum", required:false, title:"Select Hue Lights to add (${numFound} found)", multiple:true, submitOnChange: true, options:newLights
|
||||||
paragraph title: "Previously added Hue Lights (${existingLights.size()} added)", existingLightsDescription
|
paragraph title: "Previously added Hue Lights (${existingLights.size()} added)", existingLightsDescription
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
@@ -260,10 +256,6 @@ Map bulbsDiscovered() {
|
|||||||
return bulbmap
|
return bulbmap
|
||||||
}
|
}
|
||||||
|
|
||||||
def bulbListData(evt) {
|
|
||||||
state.bulbs = evt.jsonData
|
|
||||||
}
|
|
||||||
|
|
||||||
Map getHueBulbs() {
|
Map getHueBulbs() {
|
||||||
state.bulbs = state.bulbs ?: [:]
|
state.bulbs = state.bulbs ?: [:]
|
||||||
}
|
}
|
||||||
@@ -317,29 +309,6 @@ def uninstalled(){
|
|||||||
state.username = null
|
state.username = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles events to add new bulbs
|
|
||||||
def bulbListHandler(hub, data = "") {
|
|
||||||
def msg = "Bulbs list not processed. Only while in settings menu."
|
|
||||||
def bulbs = [:]
|
|
||||||
if (state.inBulbDiscovery) {
|
|
||||||
def logg = ""
|
|
||||||
log.trace "Adding bulbs to state..."
|
|
||||||
state.bridgeProcessedLightList = true
|
|
||||||
def object = new groovy.json.JsonSlurper().parseText(data)
|
|
||||||
object.each { k,v ->
|
|
||||||
if (v instanceof Map)
|
|
||||||
bulbs[k] = [id: k, name: v.name, type: v.type, modelid: v.modelid, hub:hub]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
def bridge = null
|
|
||||||
if (selectedHue) {
|
|
||||||
bridge = getChildDevice(selectedHue)
|
|
||||||
}
|
|
||||||
bridge.sendEvent(name: "bulbList", value: hub, data: bulbs, isStateChange: true, displayed: false)
|
|
||||||
msg = "${bulbs.size()} bulbs found. ${bulbs}"
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
|
|
||||||
private upgradeDeviceType(device, newHueType) {
|
private upgradeDeviceType(device, newHueType) {
|
||||||
def deviceType = getDeviceType(newHueType)
|
def deviceType = getDeviceType(newHueType)
|
||||||
|
|
||||||
@@ -448,7 +417,6 @@ def addBridge() {
|
|||||||
updateBridgeStatus(childDevice)
|
updateBridgeStatus(childDevice)
|
||||||
childDevice.sendEvent(name: "idNumber", value: idNumber)
|
childDevice.sendEvent(name: "idNumber", value: idNumber)
|
||||||
|
|
||||||
|
|
||||||
if (vbridge.value.ip && vbridge.value.port) {
|
if (vbridge.value.ip && vbridge.value.port) {
|
||||||
if (vbridge.value.ip.contains(".")) {
|
if (vbridge.value.ip.contains(".")) {
|
||||||
childDevice.sendEvent(name: "networkAddress", value: vbridge.value.ip + ":" + vbridge.value.port)
|
childDevice.sendEvent(name: "networkAddress", value: vbridge.value.ip + ":" + vbridge.value.port)
|
||||||
@@ -492,24 +460,25 @@ def ssdpBridgeHandler(evt) {
|
|||||||
def host = ip + ":" + port
|
def host = ip + ":" + port
|
||||||
log.debug "Device ($parsedEvent.mac) was already found in state with ip = $host."
|
log.debug "Device ($parsedEvent.mac) was already found in state with ip = $host."
|
||||||
def dstate = bridges."${parsedEvent.ssdpUSN.toString()}"
|
def dstate = bridges."${parsedEvent.ssdpUSN.toString()}"
|
||||||
def dni = "${parsedEvent.mac}"
|
def dniReceived = "${parsedEvent.mac}"
|
||||||
def d = getChildDevice(dni)
|
def currentDni = dstate.mac
|
||||||
|
def d = getChildDevice(dniReceived)
|
||||||
def networkAddress = null
|
def networkAddress = null
|
||||||
if (!d) {
|
if (!d) {
|
||||||
childDevices.each {
|
// There might be a mismatch between bridge DNI and the actual bridge mac address, correct that
|
||||||
if (it.getDeviceDataByName("mac")) {
|
log.debug "Bridge with $dniReceived not found"
|
||||||
def newDNI = "${it.getDeviceDataByName("mac")}"
|
def bridge = childDevices.find { it.deviceNetworkId == currentDni }
|
||||||
d = it
|
if (bridge != null) {
|
||||||
if (newDNI != it.deviceNetworkId) {
|
log.warn "Bridge is set to ${bridge.deviceNetworkId}, updating to $dniReceived"
|
||||||
def oldDNI = it.deviceNetworkId
|
bridge.setDeviceNetworkId("${dniReceived}")
|
||||||
log.debug "updating dni for device ${it} with $newDNI - previous DNI = ${it.deviceNetworkId}"
|
dstate.mac = dniReceived
|
||||||
it.setDeviceNetworkId("${newDNI}")
|
// Check to see if selectedHue is a valid bridge, otherwise update it
|
||||||
if (oldDNI == selectedHue) {
|
def isSelectedValid = bridges?.find {it.value?.mac == selectedHue}
|
||||||
app.updateSetting("selectedHue", newDNI)
|
if (isSelectedValid == null) {
|
||||||
}
|
log.warn "Correcting selectedHue in state"
|
||||||
doDeviceSync()
|
app.updateSetting("selectedHue", dniReceived)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
doDeviceSync()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updateBridgeStatus(d)
|
updateBridgeStatus(d)
|
||||||
@@ -527,6 +496,18 @@ def ssdpBridgeHandler(evt) {
|
|||||||
d.sendEvent(name:"networkAddress", value: host)
|
d.sendEvent(name:"networkAddress", value: host)
|
||||||
d.updateDataValue("networkAddress", host)
|
d.updateDataValue("networkAddress", host)
|
||||||
}
|
}
|
||||||
|
if (dstate.mac != dniReceived) {
|
||||||
|
log.warn "Correcting bridge mac address in state"
|
||||||
|
dstate.mac = dniReceived
|
||||||
|
}
|
||||||
|
if (selectedHue != dniReceived) {
|
||||||
|
// Check to see if selectedHue is a valid bridge, otherwise update it
|
||||||
|
def isSelectedValid = bridges?.find {it.value?.mac == selectedHue}
|
||||||
|
if (isSelectedValid == null) {
|
||||||
|
log.warn "Correcting selectedHue in state"
|
||||||
|
app.updateSetting("selectedHue", dniReceived)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -559,11 +540,8 @@ void lightsHandler(physicalgraph.device.HubResponse hubResponse) {
|
|||||||
if (isValidSource(hubResponse.mac)) {
|
if (isValidSource(hubResponse.mac)) {
|
||||||
def body = hubResponse.json
|
def body = hubResponse.json
|
||||||
if (!body?.state?.on) { //check if first time poll made it here by mistake
|
if (!body?.state?.on) { //check if first time poll made it here by mistake
|
||||||
def bulbs = getHueBulbs()
|
|
||||||
log.debug "Adding bulbs to state!"
|
log.debug "Adding bulbs to state!"
|
||||||
body.each { k, v ->
|
updateBulbState(body, hubResponse.hubId)
|
||||||
bulbs[k] = [id: k, name: v.name, type: v.type, modelid: v.modelid, hub: hubResponse.hubId]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -649,8 +627,7 @@ def locationHandler(evt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else if (parsedEvent.headers && parsedEvent.body) {
|
||||||
else if (parsedEvent.headers && parsedEvent.body) {
|
|
||||||
log.trace "HUE BRIDGE RESPONSES"
|
log.trace "HUE BRIDGE RESPONSES"
|
||||||
def headerString = parsedEvent.headers.toString()
|
def headerString = parsedEvent.headers.toString()
|
||||||
if (headerString?.contains("xml")) {
|
if (headerString?.contains("xml")) {
|
||||||
@@ -680,11 +657,8 @@ def locationHandler(evt) {
|
|||||||
} else {
|
} else {
|
||||||
//GET /api/${state.username}/lights response (application/json)
|
//GET /api/${state.username}/lights response (application/json)
|
||||||
if (!body?.state?.on) { //check if first time poll made it here by mistake
|
if (!body?.state?.on) { //check if first time poll made it here by mistake
|
||||||
def bulbs = getHueBulbs()
|
|
||||||
log.debug "Adding bulbs to state!"
|
log.debug "Adding bulbs to state!"
|
||||||
body.each { k,v ->
|
updateBulbState(body, parsedEvent.hub)
|
||||||
bulbs[k] = [id: k, name: v.name, type: v.type, modelid: v.modelid, hub:parsedEvent.hub]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -727,13 +701,13 @@ private void updateBridgeStatus(childDevice) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if all Hue bridges have been heard from in the last 16 minutes, if not an Offline event will be sent
|
* Check if all Hue bridges have been heard from in the last 11 minutes, if not an Offline event will be sent
|
||||||
* for the bridge. Also, set ID number on bridge if not done previously.
|
* for the bridge and all connected lights. Also, set ID number on bridge if not done previously.
|
||||||
*/
|
*/
|
||||||
private void checkBridgeStatus() {
|
private void checkBridgeStatus() {
|
||||||
def bridges = getHueBridges()
|
def bridges = getHueBridges()
|
||||||
// Check if each bridge has been heard from within the last 16 minutes (3 poll intervals times 5 minutes plus buffer)
|
// Check if each bridge has been heard from within the last 11 minutes (2 poll intervals times 5 minutes plus buffer)
|
||||||
def time = now() - (1000 * 60 * 16)
|
def time = now() - (1000 * 60 * 11)
|
||||||
bridges.each {
|
bridges.each {
|
||||||
def d = getChildDevice(it.value.mac)
|
def d = getChildDevice(it.value.mac)
|
||||||
if(d) {
|
if(d) {
|
||||||
@@ -743,14 +717,21 @@ private void checkBridgeStatus() {
|
|||||||
d.sendEvent(name: "idNumber", value: it.value.idNumber)
|
d.sendEvent(name: "idNumber", value: it.value.idNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (it.value.lastActivity < time) { // it.value.lastActivity != null &&
|
if (it.value.lastActivity < time) { // it.value.lastActivity != null &&
|
||||||
log.warn "Bridge $it.key is Offline"
|
log.warn "Bridge $it.value.idNumber is Offline"
|
||||||
d.sendEvent(name: "status", value: "Offline")
|
d.sendEvent(name: "status", value: "Offline")
|
||||||
} else {
|
|
||||||
|
state.bulbs?.each {
|
||||||
|
it.value.online = false
|
||||||
|
}
|
||||||
|
getChildDevices().each {
|
||||||
|
it.sendEvent(name: "DeviceWatch-DeviceOffline", value: "offline", isStateChange: true, displayed: false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
d.sendEvent(name: "status", value: "Online")//setOnline(false)
|
d.sendEvent(name: "status", value: "Online")//setOnline(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def isValidSource(macAddress) {
|
def isValidSource(macAddress) {
|
||||||
@@ -758,6 +739,35 @@ def isValidSource(macAddress) {
|
|||||||
return (vbridges?.find {"${it.value.mac}" == macAddress}) != null
|
return (vbridges?.find {"${it.value.mac}" == macAddress}) != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def isInBulbDiscovery() {
|
||||||
|
return state.inBulbDiscovery
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateBulbState(messageBody, hub) {
|
||||||
|
def bulbs = getHueBulbs()
|
||||||
|
|
||||||
|
// Copy of bulbs used to locate old lights in state that are no longer on bridge
|
||||||
|
def toRemove = [:]
|
||||||
|
toRemove << bulbs
|
||||||
|
|
||||||
|
messageBody.each { k,v ->
|
||||||
|
|
||||||
|
if (v instanceof Map) {
|
||||||
|
if (bulbs[k] == null) {
|
||||||
|
bulbs[k] = [:]
|
||||||
|
}
|
||||||
|
bulbs[k] << [id: k, name: v.name, type: v.type, modelid: v.modelid, hub:hub, remove: false]
|
||||||
|
toRemove.remove(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove bulbs from state that are no longer discovered
|
||||||
|
toRemove.each { k,v ->
|
||||||
|
log.warn "${bulbs[k].name} no longer exists on bridge, removing"
|
||||||
|
bulbs.remove(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
//CHILD DEVICE METHODS
|
//CHILD DEVICE METHODS
|
||||||
/////////////////////////////////////
|
/////////////////////////////////////
|
||||||
@@ -781,8 +791,7 @@ def parse(childDevice, description) {
|
|||||||
if (body instanceof java.util.Map) {
|
if (body instanceof java.util.Map) {
|
||||||
// get (poll) reponse
|
// get (poll) reponse
|
||||||
return handlePoll(body)
|
return handlePoll(body)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
//put response
|
//put response
|
||||||
return handleCommandResponse(body)
|
return handleCommandResponse(body)
|
||||||
}
|
}
|
||||||
@@ -794,10 +803,12 @@ def parse(childDevice, description) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Philips Hue priority for color is xy > ct > hs
|
// Philips Hue priority for color is xy > ct > hs
|
||||||
|
// For SmartThings, try to always send hue, sat and hex
|
||||||
private sendColorEvents(device, xy, hue, sat, ct, colormode = null) {
|
private sendColorEvents(device, xy, hue, sat, ct, colormode = null) {
|
||||||
if (device == null || (xy == null && hue == null && sat == null && ct == null))
|
if (device == null || (xy == null && hue == null && sat == null && ct == null))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def events = [:]
|
||||||
// For now, only care about changing color temperature if requested by user
|
// For now, only care about changing color temperature if requested by user
|
||||||
if (ct != null && (colormode == "ct" || (xy == null && hue == null && sat == null))) {
|
if (ct != null && (colormode == "ct" || (xy == null && hue == null && sat == null))) {
|
||||||
// for some reason setting Hue to their specified minimum off 153 yields 154, dealt with below
|
// for some reason setting Hue to their specified minimum off 153 yields 154, dealt with below
|
||||||
@@ -811,13 +822,13 @@ private sendColorEvents(device, xy, hue, sat, ct, colormode = null) {
|
|||||||
if (hue != null) {
|
if (hue != null) {
|
||||||
// 0-65535
|
// 0-65535
|
||||||
def value = Math.min(Math.round(hue * 100 / 65535), 65535) as int
|
def value = Math.min(Math.round(hue * 100 / 65535), 65535) as int
|
||||||
device.sendEvent([name: "hue", value: value, descriptionText: "Color has changed"])
|
events["hue"] = [name: "hue", value: value, descriptionText: "Color has changed", displayed: false]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sat != null) {
|
if (sat != null) {
|
||||||
// 0-254
|
// 0-254
|
||||||
def value = Math.round(sat * 100 / 254) as int
|
def value = Math.round(sat * 100 / 254) as int
|
||||||
device.sendEvent([name: "saturation", value: value, descriptionText: "Color has changed"])
|
events["saturation"] = [name: "saturation", value: value, descriptionText: "Color has changed", displayed: false]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Following is used to decide what to base hex calculations on since it is preferred to return a colorchange in hex
|
// Following is used to decide what to base hex calculations on since it is preferred to return a colorchange in hex
|
||||||
@@ -829,17 +840,28 @@ private sendColorEvents(device, xy, hue, sat, ct, colormode = null) {
|
|||||||
def model = state.bulbs[id]?.modelid
|
def model = state.bulbs[id]?.modelid
|
||||||
def hex = colorFromXY(xy, model)
|
def hex = colorFromXY(xy, model)
|
||||||
|
|
||||||
// TODO Disabled until a solution for the jumping color picker can be figured out
|
// Create Hue and Saturation events if not previously existing
|
||||||
//device.sendEvent([name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed", displayed: false])
|
def hsv = hexToHsv(hex)
|
||||||
|
if (events["hue"] == null)
|
||||||
|
events["hue"] = [name: "hue", value: hsv[0], descriptionText: "Color has changed", displayed: false]
|
||||||
|
if (events["saturation"] == null)
|
||||||
|
events["saturation"] = [name: "saturation", value: hsv[1], descriptionText: "Color has changed", displayed: false]
|
||||||
|
|
||||||
|
events["color"] = [name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed", displayed: true]
|
||||||
} else if (colormode == "hs" || colormode == null) {
|
} else if (colormode == "hs" || colormode == null) {
|
||||||
// colormode is "hs" or "xy" is missing, default to follow hue/sat which is already handled above
|
// colormode is "hs" or "xy" is missing, default to follow hue/sat which is already handled above
|
||||||
|
def hueValue = (hue != null) ? events["hue"].value : Integer.parseInt("$device.currentHue")
|
||||||
|
def satValue = (sat != null) ? events["saturation"].value : Integer.parseInt("$device.currentSaturation")
|
||||||
|
|
||||||
// TODO Disabled until the standard behavior of lights is defined (hue and sat events are sent above)
|
|
||||||
//def hex = colorUtil.hslToHex((int) device.currentHue, (int) device.currentSaturation)
|
def hex = hsvToHex(hueValue, satValue)
|
||||||
// device.sendEvent([name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed"])
|
events["color"] = [name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed", displayed: true]
|
||||||
}
|
}
|
||||||
|
|
||||||
return debug
|
boolean sendColorChanged = false
|
||||||
|
events.each {
|
||||||
|
device.sendEvent(it.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendBasicEvents(device, param, value) {
|
private sendBasicEvents(device, param, value) {
|
||||||
@@ -879,36 +901,38 @@ private handleCommandResponse(body) {
|
|||||||
// scan entire response before sending events to make sure they are always in the same order
|
// scan entire response before sending events to make sure they are always in the same order
|
||||||
def updates = [:]
|
def updates = [:]
|
||||||
|
|
||||||
body.each { payload ->
|
body.each { payload ->
|
||||||
log.debug $payload
|
|
||||||
|
|
||||||
if (payload?.success) {
|
if (payload?.success) {
|
||||||
def childDeviceNetworkId = app.id + "/"
|
def childDeviceNetworkId = app.id + "/"
|
||||||
def eventType
|
def eventType
|
||||||
payload.success.each { k, v ->
|
payload.success.each { k, v ->
|
||||||
def data = k.split("/")
|
def data = k.split("/")
|
||||||
if (data.length == 5) {
|
if (data.length == 5) {
|
||||||
childDeviceNetworkId = app.id + "/" + k.split("/")[2]
|
childDeviceNetworkId = app.id + "/" + k.split("/")[2]
|
||||||
if (!updates[childDeviceNetworkId])
|
if (!updates[childDeviceNetworkId])
|
||||||
updates[childDeviceNetworkId] = [:]
|
updates[childDeviceNetworkId] = [:]
|
||||||
eventType = k.split("/")[4]
|
eventType = k.split("/")[4]
|
||||||
updates[childDeviceNetworkId]."$eventType" = v
|
updates[childDeviceNetworkId]."$eventType" = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (payload.error) {
|
} else if (payload.error) {
|
||||||
log.warn "Error returned from Hue bridge error = ${body?.error}"
|
log.warn "Error returned from Hue bridge error = ${body?.error}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// send events for each update found above (order of events should be same as handlePoll())
|
// send events for each update found above (order of events should be same as handlePoll())
|
||||||
updates.each { childDeviceNetworkId, params ->
|
updates.each { childDeviceNetworkId, params ->
|
||||||
def device = getChildDevice(childDeviceNetworkId)
|
def device = getChildDevice(childDeviceNetworkId)
|
||||||
sendBasicEvents(device, "on", params.on)
|
def id = getId(device)
|
||||||
sendBasicEvents(device, "bri", params.bri)
|
// If device is offline, then don't send events which will update device watch
|
||||||
sendColorEvents(device, params.xy, params.hue, params.sat, params.ct)
|
if (isOnline(id)) {
|
||||||
}
|
sendBasicEvents(device, "on", params.on)
|
||||||
|
sendBasicEvents(device, "bri", params.bri)
|
||||||
|
sendColorEvents(device, params.xy, params.hue, params.sat, params.ct)
|
||||||
|
}
|
||||||
|
}
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles a response to a poll (GET) sent to the Hue Bridge.
|
* Handles a response to a poll (GET) sent to the Hue Bridge.
|
||||||
@@ -928,26 +952,53 @@ private handleCommandResponse(body) {
|
|||||||
* @return empty array
|
* @return empty array
|
||||||
*/
|
*/
|
||||||
private handlePoll(body) {
|
private handlePoll(body) {
|
||||||
if (state.updating) {
|
// Used to track "unreachable" time
|
||||||
// If user just executed commands, then ignore poll to not confuse the turning on/off state
|
// Device is considered "offline" if it has been in the "unreachable" state for
|
||||||
return []
|
// 11 minutes (e.g. two poll intervals)
|
||||||
}
|
// Note, Hue Bridge marks devices as "unreachable" often even when they accept commands
|
||||||
|
Calendar time11 = Calendar.getInstance()
|
||||||
|
time11.add(Calendar.MINUTE, -11)
|
||||||
|
Calendar currentTime = Calendar.getInstance()
|
||||||
|
|
||||||
def bulbs = getChildDevices()
|
def bulbs = getChildDevices()
|
||||||
for (bulb in body) {
|
for (bulb in body) {
|
||||||
def device = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"}
|
def device = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"}
|
||||||
if (device) {
|
if (device) {
|
||||||
if (bulb.value.state?.reachable) {
|
if (bulb.value.state?.reachable) {
|
||||||
sendBasicEvents(device, "on", bulb.value?.state?.on)
|
if (state.bulbs[bulb.key]?.online == false) {
|
||||||
sendBasicEvents(device, "bri", bulb.value?.state?.bri)
|
// light just came back online, notify device watch
|
||||||
sendColorEvents(device, bulb.value?.state?.xy, bulb.value?.state?.hue, bulb.value?.state?.sat, bulb.value?.state?.ct, bulb.value?.state?.colormode)
|
def lastActivity = now()
|
||||||
|
device.sendEvent(name: "deviceWatch-status", value: "ONLINE", description: "Last Activity is on ${new Date((long) lastActivity)}", displayed: false, isStateChange: true)
|
||||||
|
log.debug "$device is Online"
|
||||||
|
}
|
||||||
|
// Mark light as "online"
|
||||||
|
state.bulbs[bulb.key]?.unreachableSince = null
|
||||||
|
state.bulbs[bulb.key]?.online = true
|
||||||
|
|
||||||
|
// If user just executed commands, then do not send events to avoid confusing the turning on/off state
|
||||||
|
if (!state.updating) {
|
||||||
|
sendBasicEvents(device, "on", bulb.value?.state?.on)
|
||||||
|
sendBasicEvents(device, "bri", bulb.value?.state?.bri)
|
||||||
|
sendColorEvents(device, bulb.value?.state?.xy, bulb.value?.state?.hue, bulb.value?.state?.sat, bulb.value?.state?.ct, bulb.value?.state?.colormode)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.warn "$device is not reachable by Hue bridge"
|
if (state.bulbs[bulb.key]?.unreachableSince == null) {
|
||||||
}
|
// Store the first time where device was reported as "unreachable"
|
||||||
}
|
state.bulbs[bulb.key]?.unreachableSince = currentTime.getTimeInMillis()
|
||||||
}
|
} else if (state.bulbs[bulb.key]?.online) {
|
||||||
return []
|
// Check if device was "unreachable" for more than 11 minutes and mark "offline" if necessary
|
||||||
|
if (state.bulbs[bulb.key]?.unreachableSince < time11.getTimeInMillis()) {
|
||||||
|
log.warn "$device went Offline"
|
||||||
|
state.bulbs[bulb.key]?.online = false
|
||||||
|
device.sendEvent(name: "DeviceWatch-DeviceOffline", value: "offline", displayed: false, isStateChange: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.warn "$device may not reachable by Hue bridge"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
private updateInProgress() {
|
private updateInProgress() {
|
||||||
state.updating = true
|
state.updating = true
|
||||||
@@ -976,22 +1027,25 @@ def hubVerification(bodytext) {
|
|||||||
|
|
||||||
def on(childDevice) {
|
def on(childDevice) {
|
||||||
log.debug "Executing 'on'"
|
log.debug "Executing 'on'"
|
||||||
|
def id = getId(childDevice)
|
||||||
updateInProgress()
|
updateInProgress()
|
||||||
createSwitchEvent(childDevice, "on")
|
createSwitchEvent(childDevice, "on")
|
||||||
put("lights/${getId(childDevice)}/state", [on: true])
|
put("lights/$id/state", [on: true])
|
||||||
return "Bulb is turning On"
|
return "Bulb is turning On"
|
||||||
}
|
}
|
||||||
|
|
||||||
def off(childDevice) {
|
def off(childDevice) {
|
||||||
log.debug "Executing 'off'"
|
log.debug "Executing 'off'"
|
||||||
|
def id = getId(childDevice)
|
||||||
updateInProgress()
|
updateInProgress()
|
||||||
createSwitchEvent(childDevice, "off")
|
createSwitchEvent(childDevice, "off")
|
||||||
put("lights/${getId(childDevice)}/state", [on: false])
|
put("lights/$id/state", [on: false])
|
||||||
return "Bulb is turning Off"
|
return "Bulb is turning Off"
|
||||||
}
|
}
|
||||||
|
|
||||||
def setLevel(childDevice, percent) {
|
def setLevel(childDevice, percent) {
|
||||||
log.debug "Executing 'setLevel'"
|
log.debug "Executing 'setLevel'"
|
||||||
|
def id = getId(childDevice)
|
||||||
updateInProgress()
|
updateInProgress()
|
||||||
// 1 - 254
|
// 1 - 254
|
||||||
def level
|
def level
|
||||||
@@ -1006,48 +1060,51 @@ def setLevel(childDevice, percent) {
|
|||||||
// that means that the light will still be on when on is called next time
|
// that means that the light will still be on when on is called next time
|
||||||
// Lets emulate that here
|
// Lets emulate that here
|
||||||
if (percent > 0) {
|
if (percent > 0) {
|
||||||
put("lights/${getId(childDevice)}/state", [bri: level, on: true])
|
put("lights/$id/state", [bri: level, on: true])
|
||||||
} else {
|
} else {
|
||||||
put("lights/${getId(childDevice)}/state", [on: false])
|
put("lights/$id/state", [on: false])
|
||||||
}
|
}
|
||||||
return "Setting level to $percent"
|
return "Setting level to $percent"
|
||||||
}
|
}
|
||||||
|
|
||||||
def setSaturation(childDevice, percent) {
|
def setSaturation(childDevice, percent) {
|
||||||
log.debug "Executing 'setSaturation($percent)'"
|
log.debug "Executing 'setSaturation($percent)'"
|
||||||
updateInProgress()
|
def id = getId(childDevice)
|
||||||
|
updateInProgress()
|
||||||
// 0 - 254
|
// 0 - 254
|
||||||
def level = Math.min(Math.round(percent * 254 / 100), 254)
|
def level = Math.min(Math.round(percent * 254 / 100), 254)
|
||||||
// TODO should this be done by app only or should we default to on?
|
// TODO should this be done by app only or should we default to on?
|
||||||
createSwitchEvent(childDevice, "on")
|
createSwitchEvent(childDevice, "on")
|
||||||
put("lights/${getId(childDevice)}/state", [sat: level, on: true])
|
put("lights/$id/state", [sat: level, on: true])
|
||||||
return "Setting saturation to $percent"
|
return "Setting saturation to $percent"
|
||||||
}
|
}
|
||||||
|
|
||||||
def setHue(childDevice, percent) {
|
def setHue(childDevice, percent) {
|
||||||
log.debug "Executing 'setHue($percent)'"
|
log.debug "Executing 'setHue($percent)'"
|
||||||
|
def id = getId(childDevice)
|
||||||
updateInProgress()
|
updateInProgress()
|
||||||
// 0 - 65535
|
// 0 - 65535
|
||||||
def level = Math.min(Math.round(percent * 65535 / 100), 65535)
|
def level = Math.min(Math.round(percent * 65535 / 100), 65535)
|
||||||
// TODO should this be done by app only or should we default to on?
|
// TODO should this be done by app only or should we default to on?
|
||||||
createSwitchEvent(childDevice, "on")
|
createSwitchEvent(childDevice, "on")
|
||||||
put("lights/${getId(childDevice)}/state", [hue: level, on: true])
|
put("lights/$id/state", [hue: level, on: true])
|
||||||
return "Setting hue to $percent"
|
return "Setting hue to $percent"
|
||||||
}
|
}
|
||||||
|
|
||||||
def setColorTemperature(childDevice, huesettings) {
|
def setColorTemperature(childDevice, huesettings) {
|
||||||
log.debug "Executing 'setColorTemperature($huesettings)'"
|
log.debug "Executing 'setColorTemperature($huesettings)'"
|
||||||
|
def id = getId(childDevice)
|
||||||
updateInProgress()
|
updateInProgress()
|
||||||
// 153 (6500K) to 500 (2000K)
|
// 153 (6500K) to 500 (2000K)
|
||||||
def ct = hueSettings == 6500 ? 153 : Math.round(1000000/huesettings)
|
def ct = hueSettings == 6500 ? 153 : Math.round(1000000/huesettings)
|
||||||
createSwitchEvent(childDevice, "on")
|
createSwitchEvent(childDevice, "on")
|
||||||
put("lights/${getId(childDevice)}/state", [ct: ct, on: true])
|
put("lights/$id/state", [ct: ct, on: true])
|
||||||
return "Setting color temperature to $percent"
|
return "Setting color temperature to $percent"
|
||||||
}
|
}
|
||||||
|
|
||||||
def setColor(childDevice, huesettings) {
|
def setColor(childDevice, huesettings) {
|
||||||
log.debug "Executing 'setColor($huesettings)'"
|
log.debug "Executing 'setColor($huesettings)'"
|
||||||
|
def id = getId(childDevice)
|
||||||
updateInProgress()
|
updateInProgress()
|
||||||
|
|
||||||
def value = [:]
|
def value = [:]
|
||||||
@@ -1055,26 +1112,22 @@ def setColor(childDevice, huesettings) {
|
|||||||
def sat = null
|
def sat = null
|
||||||
def xy = null
|
def xy = null
|
||||||
|
|
||||||
// For now ignore model to get a consistent color if same color is set across multiple devices
|
// Prefer hue/sat over hex to make sure it works with the majority of the smartapps
|
||||||
// def model = state.bulbs[getId(childDevice)]?.modelid
|
if (huesettings.hue != null || huesettings.sat != null) {
|
||||||
if (huesettings.hex != null) {
|
// If both hex and hue/sat are set, send all values to bridge to get hue/sat in response from bridge to
|
||||||
|
// generate hue/sat events even though bridge will prioritize XY when setting color
|
||||||
|
if (huesettings.hue != null)
|
||||||
|
value.hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
|
||||||
|
if (huesettings.saturation != null)
|
||||||
|
value.sat = Math.min(Math.round(huesettings.saturation * 254 / 100), 254)
|
||||||
|
} else if (huesettings.hex != null) {
|
||||||
|
// For now ignore model to get a consistent color if same color is set across multiple devices
|
||||||
|
// def model = state.bulbs[getId(childDevice)]?.modelid
|
||||||
// value.xy = calculateXY(huesettings.hex, model)
|
// value.xy = calculateXY(huesettings.hex, model)
|
||||||
// Once groups, or scenes are introduced it might be a good idea to use unique models again
|
// Once groups, or scenes are introduced it might be a good idea to use unique models again
|
||||||
value.xy = calculateXY(huesettings.hex)
|
value.xy = calculateXY(huesettings.hex)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If both hex and hue/sat are set, send all values to bridge to get hue/sat in response from bridge to
|
|
||||||
// generate hue/sat events even though bridge will prioritize XY when setting color
|
|
||||||
if (huesettings.hue != null)
|
|
||||||
value.hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
|
|
||||||
else
|
|
||||||
value.hue = Math.min(Math.round(childDevice.device?.currentValue("hue") * 65535 / 100), 65535)
|
|
||||||
|
|
||||||
if (huesettings.saturation != null)
|
|
||||||
value.sat = Math.min(Math.round(huesettings.saturation * 254 / 100), 254)
|
|
||||||
else
|
|
||||||
value.sat = Math.min(Math.round(childDevice.device?.currentValue("saturation") * 254 / 100), 254)
|
|
||||||
|
|
||||||
/* Disabled for now due to bad behavior via Lightning Wizard
|
/* Disabled for now due to bad behavior via Lightning Wizard
|
||||||
if (!value.xy) {
|
if (!value.xy) {
|
||||||
// Below will translate values to hex->XY to take into account the color support of the different hue types
|
// Below will translate values to hex->XY to take into account the color support of the different hue types
|
||||||
@@ -1104,15 +1157,30 @@ def setColor(childDevice, huesettings) {
|
|||||||
value.on = false
|
value.on = false
|
||||||
|
|
||||||
createSwitchEvent(childDevice, value.on ? "on" : "off")
|
createSwitchEvent(childDevice, value.on ? "on" : "off")
|
||||||
put("lights/${getId(childDevice)}/state", value)
|
put("lights/$id/state", value)
|
||||||
return "Setting color to $value"
|
return "Setting color to $value"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def ping(childDevice) {
|
||||||
|
if (childDevice.device?.deviceNetworkId?.equalsIgnoreCase(selectedHue)) {
|
||||||
|
if (childDevice.device?.currentValue("status")?.equalsIgnoreCase("Online")) {
|
||||||
|
childDevice.sendEvent(name: "deviceWatch-ping", value: "ONLINE", description: "Hue Bridge is reachable", displayed: false, isStateChange: true)
|
||||||
|
return "Bridge is Online"
|
||||||
|
} else {
|
||||||
|
return "Bridge is Offline"
|
||||||
|
}
|
||||||
|
} else if (isOnline(getId(childDevice))) {
|
||||||
|
childDevice.sendEvent(name: "deviceWatch-ping", value: "ONLINE", description: "Hue Light is reachable", displayed: false, isStateChange: true)
|
||||||
|
return "Device is Online"
|
||||||
|
} else {
|
||||||
|
return "Device is Offline"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private getId(childDevice) {
|
private getId(childDevice) {
|
||||||
if (childDevice.device?.deviceNetworkId?.startsWith("HUE")) {
|
if (childDevice.device?.deviceNetworkId?.startsWith("HUE")) {
|
||||||
return childDevice.device?.deviceNetworkId[3..-1]
|
return childDevice.device?.deviceNetworkId[3..-1]
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
return childDevice.device?.deviceNetworkId.split("/")[-1]
|
return childDevice.device?.deviceNetworkId.split("/")[-1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1121,10 +1189,12 @@ private poll() {
|
|||||||
def host = getBridgeIP()
|
def host = getBridgeIP()
|
||||||
def uri = "/api/${state.username}/lights/"
|
def uri = "/api/${state.username}/lights/"
|
||||||
log.debug "GET: $host$uri"
|
log.debug "GET: $host$uri"
|
||||||
sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1
|
sendHubCommand(new physicalgraph.device.HubAction("GET ${uri} HTTP/1.1\r\n" +
|
||||||
HOST: ${host}
|
"HOST: ${host}\r\n\r\n", physicalgraph.device.Protocol.LAN, selectedHue))
|
||||||
|
}
|
||||||
|
|
||||||
""", physicalgraph.device.Protocol.LAN, selectedHue))
|
private isOnline(id) {
|
||||||
|
return (state.bulbs[id]?.online != null && state.bulbs[id]?.online) || state.bulbs[id]?.online == null
|
||||||
}
|
}
|
||||||
|
|
||||||
private put(path, body) {
|
private put(path, body) {
|
||||||
@@ -1136,13 +1206,11 @@ private put(path, body) {
|
|||||||
log.debug "PUT: $host$uri"
|
log.debug "PUT: $host$uri"
|
||||||
log.debug "BODY: ${bodyJSON}"
|
log.debug "BODY: ${bodyJSON}"
|
||||||
|
|
||||||
sendHubCommand(new physicalgraph.device.HubAction("""PUT $uri HTTP/1.1
|
sendHubCommand(new physicalgraph.device.HubAction("PUT $uri HTTP/1.1\r\n" +
|
||||||
HOST: ${host}
|
"HOST: ${host}\r\n" +
|
||||||
Content-Length: ${length}
|
"Content-Length: ${length}\r\n" +
|
||||||
|
"\r\n" +
|
||||||
${bodyJSON}
|
"${bodyJSON}", physicalgraph.device.Protocol.LAN, "${selectedHue}"))
|
||||||
""", physicalgraph.device.Protocol.LAN, "${selectedHue}"))
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -1163,7 +1231,7 @@ private getBridgeIP() {
|
|||||||
if (d) {
|
if (d) {
|
||||||
if (d.getDeviceDataByName("networkAddress"))
|
if (d.getDeviceDataByName("networkAddress"))
|
||||||
host = d.getDeviceDataByName("networkAddress")
|
host = d.getDeviceDataByName("networkAddress")
|
||||||
else
|
else
|
||||||
host = d.latestState('networkAddress').stringValue
|
host = d.latestState('networkAddress').stringValue
|
||||||
}
|
}
|
||||||
if (host == null || host == "") {
|
if (host == null || host == "") {
|
||||||
@@ -1193,8 +1261,8 @@ def convertBulbListToMap() {
|
|||||||
try {
|
try {
|
||||||
if (state.bulbs instanceof java.util.List) {
|
if (state.bulbs instanceof java.util.List) {
|
||||||
def map = [:]
|
def map = [:]
|
||||||
state.bulbs.unique {it.id}.each { bulb ->
|
state.bulbs?.unique {it.id}.each { bulb ->
|
||||||
map << ["${bulb.id}":["id":bulb.id, "name":bulb.name, "type": bulb.type, "modelid": bulb.modelid, "hub":bulb.hub]]
|
map << ["${bulb.id}":["id":bulb.id, "name":bulb.name, "type": bulb.type, "modelid": bulb.modelid, "hub":bulb.hub, "online": bulb.online]]
|
||||||
}
|
}
|
||||||
state.bulbs = map
|
state.bulbs = map
|
||||||
}
|
}
|
||||||
@@ -1600,3 +1668,101 @@ private boolean checkPointInLampsReach(p, colorPoints) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an RGB color in hex to HSV/HSB.
|
||||||
|
* Algorithm based on http://en.wikipedia.org/wiki/HSV_color_space.
|
||||||
|
*
|
||||||
|
* @param colorStr color value in hex (#ff03d3)
|
||||||
|
*
|
||||||
|
* @return HSV representation in an array (0-100) [hue, sat, value]
|
||||||
|
*/
|
||||||
|
def hexToHsv(colorStr){
|
||||||
|
def r = Integer.valueOf( colorStr.substring( 1, 3 ), 16 ) / 255
|
||||||
|
def g = Integer.valueOf( colorStr.substring( 3, 5 ), 16 ) / 255
|
||||||
|
def b = Integer.valueOf( colorStr.substring( 5, 7 ), 16 ) / 255
|
||||||
|
|
||||||
|
def max = Math.max(Math.max(r, g), b)
|
||||||
|
def min = Math.min(Math.min(r, g), b)
|
||||||
|
|
||||||
|
def h, s, v = max
|
||||||
|
|
||||||
|
def d = max - min
|
||||||
|
s = max == 0 ? 0 : d / max
|
||||||
|
|
||||||
|
if(max == min){
|
||||||
|
h = 0
|
||||||
|
}else{
|
||||||
|
switch(max){
|
||||||
|
case r: h = (g - b) / d + (g < b ? 6 : 0); break
|
||||||
|
case g: h = (b - r) / d + 2; break
|
||||||
|
case b: h = (r - g) / d + 4; break
|
||||||
|
}
|
||||||
|
h /= 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [Math.round(h * 100), Math.round(s * 100), Math.round(v * 100)]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts HSV/HSB color to RGB in hex.
|
||||||
|
* Algorithm based on http://en.wikipedia.org/wiki/HSV_color_space.
|
||||||
|
*
|
||||||
|
* @param hue hue 0-100
|
||||||
|
* @param sat saturation 0-100
|
||||||
|
* @param value value 0-100 (defaults to 100)
|
||||||
|
|
||||||
|
* @return the color in hex (#ff03d3)
|
||||||
|
*/
|
||||||
|
def hsvToHex(hue, sat, value = 100){
|
||||||
|
def r, g, b;
|
||||||
|
def h = hue / 100
|
||||||
|
def s = sat / 100
|
||||||
|
def v = value / 100
|
||||||
|
|
||||||
|
def i = Math.floor(h * 6)
|
||||||
|
def f = h * 6 - i
|
||||||
|
def p = v * (1 - s)
|
||||||
|
def q = v * (1 - f * s)
|
||||||
|
def t = v * (1 - (1 - f) * s)
|
||||||
|
|
||||||
|
switch (i % 6) {
|
||||||
|
case 0:
|
||||||
|
r = v
|
||||||
|
g = t
|
||||||
|
b = p
|
||||||
|
break
|
||||||
|
case 1:
|
||||||
|
r = q
|
||||||
|
g = v
|
||||||
|
b = p
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
r = p
|
||||||
|
g = v
|
||||||
|
b = t
|
||||||
|
break
|
||||||
|
case 3:
|
||||||
|
r = p
|
||||||
|
g = q
|
||||||
|
b = v
|
||||||
|
break
|
||||||
|
case 4:
|
||||||
|
r = t
|
||||||
|
g = p
|
||||||
|
b = v
|
||||||
|
break
|
||||||
|
case 5:
|
||||||
|
r = v
|
||||||
|
g = p
|
||||||
|
b = q
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converting float components to int components.
|
||||||
|
def r1 = String.format("%02X", (int) (r * 255.0f))
|
||||||
|
def g1 = String.format("%02X", (int) (g * 255.0f))
|
||||||
|
def b1 = String.format("%02X", (int) (b * 255.0f))
|
||||||
|
|
||||||
|
return "#$r1$g1$b1"
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,14 +53,14 @@ def accelerationActiveHandler(evt) {
|
|||||||
def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1
|
def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1
|
||||||
|
|
||||||
if (alreadySentSms) {
|
if (alreadySentSms) {
|
||||||
log.debug "SMS already sent to $phone1 within the last $deltaSeconds seconds"
|
log.debug "SMS already sent within the last $deltaSeconds seconds"
|
||||||
} else {
|
} else {
|
||||||
if (location.contactBookEnabled) {
|
if (location.contactBookEnabled) {
|
||||||
log.debug "$accelerationSensor has moved, texting contacts: ${recipients?.size()}"
|
log.debug "accelerationSensor has moved, texting contacts: ${recipients?.size()}"
|
||||||
sendNotificationToContacts("${accelerationSensor.label ?: accelerationSensor.name} moved", recipients)
|
sendNotificationToContacts("${accelerationSensor.label ?: accelerationSensor.name} moved", recipients)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log.debug "$accelerationSensor has moved, texting $phone1"
|
log.debug "accelerationSensor has moved, sending text message"
|
||||||
sendSms(phone1, "${accelerationSensor.label ?: accelerationSensor.name} moved")
|
sendSms(phone1, "${accelerationSensor.label ?: accelerationSensor.name} moved")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,10 +69,10 @@ def temperatureHandler(evt) {
|
|||||||
def alreadySentSms = recentEvents.count { it.doubleValue >= tooHot } > 1
|
def alreadySentSms = recentEvents.count { it.doubleValue >= tooHot } > 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 rose above $tooHot: sending SMS to $phone1 and activating $mySwitch"
|
log.debug "Temperature rose above $tooHot: sending SMS and activating $mySwitch"
|
||||||
def tempScale = location.temperatureScale ?: "F"
|
def tempScale = location.temperatureScale ?: "F"
|
||||||
send("${temperatureSensor1.displayName} is too hot, reporting a temperature of ${evt.value}${evt.unit?:tempScale}")
|
send("${temperatureSensor1.displayName} is too hot, 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
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ def authPage() {
|
|||||||
}
|
}
|
||||||
def description = "Tap to enter LIFX credentials"
|
def description = "Tap to enter LIFX credentials"
|
||||||
def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" // this triggers oauthInit() below
|
def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" // this triggers oauthInit() below
|
||||||
// def redirectUrl = "${apiServerUrl}"
|
// def redirectUrl = "${apiServerUrl}"
|
||||||
log.debug "app id: ${app.id}"
|
// log.debug "app id: ${app.id}"
|
||||||
log.debug "redirect url: ${redirectUrl}"
|
// log.debug "redirect url: ${redirectUrl}"s
|
||||||
return dynamicPage(name: "Credentials", title: "Connect to LIFX", nextPage: null, uninstall: true, install:true) {
|
return dynamicPage(name: "Credentials", title: "Connect to LIFX", nextPage: null, uninstall: true, install:true) {
|
||||||
section {
|
section {
|
||||||
href(url:redirectUrl, required:true, title:"Connect to LIFX", description:"Tap here to connect your LIFX account")
|
href(url:redirectUrl, required:true, title:"Connect to LIFX", description:"Tap here to connect your LIFX account")
|
||||||
@@ -372,7 +372,7 @@ def updateDevices() {
|
|||||||
def childDevice = getChildDevice(device.id)
|
def childDevice = getChildDevice(device.id)
|
||||||
selectors.add("${device.id}")
|
selectors.add("${device.id}")
|
||||||
if (!childDevice) {
|
if (!childDevice) {
|
||||||
log.info("Adding device ${device.id}: ${device.product}")
|
// log.info("Adding device ${device.id}: ${device.product}")
|
||||||
def data = [
|
def data = [
|
||||||
label: device.label,
|
label: device.label,
|
||||||
level: Math.round((device.brightness ?: 1) * 100),
|
level: Math.round((device.brightness ?: 1) * 100),
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
* locks | lock | lock, unlock | locked, unlocked
|
* locks | lock | lock, unlock | locked, unlocked
|
||||||
* ---------------------+-------------------+-----------------------------+------------------------------------
|
* ---------------------+-------------------+-----------------------------+------------------------------------
|
||||||
*/
|
*/
|
||||||
|
include 'asynchttp_v1'
|
||||||
|
|
||||||
definition(
|
definition(
|
||||||
name: "Logitech Harmony (Connect)",
|
name: "Logitech Harmony (Connect)",
|
||||||
@@ -51,7 +52,7 @@ definition(
|
|||||||
}
|
}
|
||||||
|
|
||||||
preferences(oauthPage: "deviceAuthorization") {
|
preferences(oauthPage: "deviceAuthorization") {
|
||||||
page(name: "Credentials", title: "Connect to your Logitech Harmony device", content: "authPage", install: false, nextPage: "deviceAuthorization")
|
page(name: "Credentials", title: "Connect to your Logitech Harmony device", content: "authPage", install: false, nextPage: "deviceAuthorization")
|
||||||
page(name: "deviceAuthorization", title: "Logitech Harmony device authorization", install: true) {
|
page(name: "deviceAuthorization", title: "Logitech Harmony device authorization", install: true) {
|
||||||
section("Allow Logitech Harmony to control these things...") {
|
section("Allow Logitech Harmony to control these things...") {
|
||||||
input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false
|
input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false
|
||||||
@@ -96,38 +97,41 @@ def authPage() {
|
|||||||
def description = null
|
def description = null
|
||||||
if (!state.HarmonyAccessToken) {
|
if (!state.HarmonyAccessToken) {
|
||||||
if (!state.accessToken) {
|
if (!state.accessToken) {
|
||||||
log.debug "About to create access token"
|
log.debug "Harmony - About to create access token"
|
||||||
createAccessToken()
|
createAccessToken()
|
||||||
}
|
}
|
||||||
description = "Click to enter Harmony Credentials"
|
description = "Click to enter Harmony Credentials"
|
||||||
def redirectUrl = buildRedirectUrl
|
def redirectUrl = buildRedirectUrl
|
||||||
return dynamicPage(name: "Credentials", title: "Harmony", nextPage: null, uninstall: true, install:false) {
|
return dynamicPage(name: "Credentials", title: "Harmony", nextPage: null, uninstall: true, install:false) {
|
||||||
section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description }
|
section { paragraph title: "Note:", "This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance." }
|
||||||
|
section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
//device discovery request every 5 //25 seconds
|
//device discovery request every 5 //25 seconds
|
||||||
int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int
|
int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int
|
||||||
state.deviceRefreshCount = deviceRefreshCount + 1
|
state.deviceRefreshCount = deviceRefreshCount + 1
|
||||||
def refreshInterval = 3
|
def refreshInterval = 5
|
||||||
|
|
||||||
def huboptions = state.HarmonyHubs ?: []
|
def huboptions = state.HarmonyHubs ?: []
|
||||||
def actoptions = state.HarmonyActivities ?: []
|
def actoptions = state.HarmonyActivities ?: []
|
||||||
|
|
||||||
def numFoundHub = huboptions.size() ?: 0
|
def numFoundHub = huboptions.size() ?: 0
|
||||||
def numFoundAct = actoptions.size() ?: 0
|
def numFoundAct = actoptions.size() ?: 0
|
||||||
|
|
||||||
if((deviceRefreshCount % 5) == 0) {
|
if((deviceRefreshCount % 5) == 0) {
|
||||||
discoverDevices()
|
discoverDevices()
|
||||||
}
|
}
|
||||||
|
|
||||||
return dynamicPage(name:"Credentials", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
return dynamicPage(name:"Credentials", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
||||||
section("Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
section("Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||||
input "selectedhubs", "enum", required:false, title:"Select Harmony Hubs (${numFoundHub} found)", multiple:true, options:huboptions
|
input "selectedhubs", "enum", required:false, title:"Select Harmony Hubs (${numFoundHub} found)", multiple:true, submitOnChange: true, options:huboptions
|
||||||
}
|
}
|
||||||
// Virtual activity flag
|
// Virtual activity flag
|
||||||
if (numFoundHub > 0 && numFoundAct > 0 && true)
|
if (numFoundHub > 0 && numFoundAct > 0 && true)
|
||||||
section("You can also add activities as virtual switches for other convenient integrations") {
|
section("You can also add activities as virtual switches for other convenient integrations") {
|
||||||
input "selectedactivities", "enum", required:false, title:"Select Harmony Activities (${numFoundAct} found)", multiple:true, options:actoptions
|
input "selectedactivities", "enum", required:false, title:"Select Harmony Activities (${numFoundAct} found)", multiple:true, submitOnChange: true, options:actoptions
|
||||||
}
|
}
|
||||||
if (state.resethub)
|
if (state.resethub)
|
||||||
section("Connection to the hub timed out. Please restart the hub and try again.") {}
|
section("Connection to the hub timed out. Please restart the hub and try again.") {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,13 +141,13 @@ def callback() {
|
|||||||
def redirectUrl = null
|
def redirectUrl = null
|
||||||
if (params.authQueryString) {
|
if (params.authQueryString) {
|
||||||
redirectUrl = URLDecoder.decode(params.authQueryString.replaceAll(".+&redirect_url=", ""))
|
redirectUrl = URLDecoder.decode(params.authQueryString.replaceAll(".+&redirect_url=", ""))
|
||||||
log.debug "redirectUrl: ${redirectUrl}"
|
log.debug "Harmony - redirectUrl: ${redirectUrl}"
|
||||||
} else {
|
} else {
|
||||||
log.warn "No authQueryString"
|
log.warn "Harmony - No authQueryString"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.HarmonyAccessToken) {
|
if (state.HarmonyAccessToken) {
|
||||||
log.debug "Access token already exists"
|
log.debug "Harmony - Access token already exists"
|
||||||
discovery()
|
discovery()
|
||||||
success()
|
success()
|
||||||
} else {
|
} else {
|
||||||
@@ -151,27 +155,27 @@ def callback() {
|
|||||||
if (code) {
|
if (code) {
|
||||||
if (code.size() > 6) {
|
if (code.size() > 6) {
|
||||||
// Harmony code
|
// Harmony code
|
||||||
log.debug "Exchanging code for access token"
|
log.debug "Harmony - Exchanging code for access token"
|
||||||
receiveToken(redirectUrl)
|
receiveToken(redirectUrl)
|
||||||
} else {
|
} else {
|
||||||
// Initiate the Harmony OAuth flow.
|
// Initiate the Harmony OAuth flow.
|
||||||
init()
|
init()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.debug "This code should be unreachable"
|
log.debug "Harmony - This code should be unreachable"
|
||||||
success()
|
success()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def init() {
|
def init() {
|
||||||
log.debug "Requesting Code"
|
log.debug "Harmony - Requesting Code"
|
||||||
def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${servercallbackUrl}" ]
|
def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${servercallbackUrl}" ]
|
||||||
redirect(location: "https://home.myharmony.com/oauth2/authorize?${toQueryString(oauthParams)}")
|
redirect(location: "https://home.myharmony.com/oauth2/authorize?${toQueryString(oauthParams)}")
|
||||||
}
|
}
|
||||||
|
|
||||||
def receiveToken(redirectUrl = null) {
|
def receiveToken(redirectUrl = null) {
|
||||||
log.debug "receiveToken"
|
log.debug "Harmony - receiveToken"
|
||||||
def oauthParams = [ client_id: "${appSettings.clientId}", client_secret: "${appSettings.clientSecret}", grant_type: "authorization_code", code: params.code ]
|
def oauthParams = [ client_id: "${appSettings.clientId}", client_secret: "${appSettings.clientSecret}", grant_type: "authorization_code", code: params.code ]
|
||||||
def params = [
|
def params = [
|
||||||
uri: "https://home.myharmony.com/oauth2/token?${toQueryString(oauthParams)}",
|
uri: "https://home.myharmony.com/oauth2/token?${toQueryString(oauthParams)}",
|
||||||
@@ -182,7 +186,7 @@ def receiveToken(redirectUrl = null) {
|
|||||||
}
|
}
|
||||||
} catch (java.util.concurrent.TimeoutException e) {
|
} catch (java.util.concurrent.TimeoutException e) {
|
||||||
fail(e)
|
fail(e)
|
||||||
log.warn "Connection timed out, please try again later."
|
log.warn "Harmony - Connection timed out, please try again later."
|
||||||
}
|
}
|
||||||
discovery()
|
discovery()
|
||||||
if (state.HarmonyAccessToken) {
|
if (state.HarmonyAccessToken) {
|
||||||
@@ -306,7 +310,7 @@ def buildRedirectUrl(page) {
|
|||||||
|
|
||||||
def installed() {
|
def installed() {
|
||||||
if (!state.accessToken) {
|
if (!state.accessToken) {
|
||||||
log.debug "About to create access token"
|
log.debug "Harmony - About to create access token"
|
||||||
createAccessToken()
|
createAccessToken()
|
||||||
} else {
|
} else {
|
||||||
initialize()
|
initialize()
|
||||||
@@ -314,10 +318,8 @@ def installed() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def updated() {
|
def updated() {
|
||||||
unsubscribe()
|
|
||||||
unschedule()
|
|
||||||
if (!state.accessToken) {
|
if (!state.accessToken) {
|
||||||
log.debug "About to create access token"
|
log.debug "Harmony - About to create access token"
|
||||||
createAccessToken()
|
createAccessToken()
|
||||||
} else {
|
} else {
|
||||||
initialize()
|
initialize()
|
||||||
@@ -328,9 +330,9 @@ def uninstalled() {
|
|||||||
if (state.HarmonyAccessToken) {
|
if (state.HarmonyAccessToken) {
|
||||||
try {
|
try {
|
||||||
state.HarmonyAccessToken = ""
|
state.HarmonyAccessToken = ""
|
||||||
log.debug "Success disconnecting Harmony from SmartThings"
|
log.debug "Harmony - Success disconnecting Harmony from SmartThings"
|
||||||
} catch (groovyx.net.http.HttpResponseException e) {
|
} catch (groovyx.net.http.HttpResponseException e) {
|
||||||
log.error "Error disconnecting Harmony from SmartThings: ${e.statusCode}"
|
log.error "Harmony - Error disconnecting Harmony from SmartThings: ${e.statusCode}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,7 +341,8 @@ def initialize() {
|
|||||||
state.aux = 0
|
state.aux = 0
|
||||||
if (selectedhubs || selectedactivities) {
|
if (selectedhubs || selectedactivities) {
|
||||||
addDevice()
|
addDevice()
|
||||||
runEvery5Minutes("poll")
|
runEvery5Minutes("poll")
|
||||||
|
getActivityList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,7 +351,7 @@ def getHarmonydevices() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Map discoverDevices() {
|
Map discoverDevices() {
|
||||||
log.trace "Discovering devices..."
|
log.trace "Harmony - Discovering devices..."
|
||||||
discovery()
|
discovery()
|
||||||
if (getHarmonydevices() != []) {
|
if (getHarmonydevices() != []) {
|
||||||
def devices = state.Harmonydevices.hubs
|
def devices = state.Harmonydevices.hubs
|
||||||
@@ -360,7 +363,7 @@ Map discoverDevices() {
|
|||||||
def hubname = getHubName(it.key)
|
def hubname = getHubName(it.key)
|
||||||
def hubvalue = "${hubname}"
|
def hubvalue = "${hubname}"
|
||||||
hubs["harmony-${hubkey}"] = hubvalue
|
hubs["harmony-${hubkey}"] = hubvalue
|
||||||
it.value.response.data.activities.each {
|
it.value.response.data.activities.each {
|
||||||
def value = "${it.value.name}"
|
def value = "${it.value.name}"
|
||||||
def key = "harmony-${hubkey}-${it.key}"
|
def key = "harmony-${hubkey}-${it.key}"
|
||||||
activities["${key}"] = value
|
activities["${key}"] = value
|
||||||
@@ -378,164 +381,177 @@ def discovery() {
|
|||||||
try {
|
try {
|
||||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
log.debug "valid Token"
|
log.debug "Harmony - valid Token"
|
||||||
state.Harmonydevices = response.data
|
state.Harmonydevices = response.data
|
||||||
state.resethub = false
|
state.resethub = false
|
||||||
getActivityList()
|
|
||||||
poll()
|
|
||||||
} else {
|
} else {
|
||||||
log.debug "Error: $response.status"
|
log.debug "Harmony - Error: $response.status"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (groovyx.net.http.HttpResponseException e) {
|
} catch (groovyx.net.http.HttpResponseException e) {
|
||||||
if (e.statusCode == 401) { // token is expired
|
if (e.statusCode == 401) { // token is expired
|
||||||
state.remove("HarmonyAccessToken")
|
state.remove("HarmonyAccessToken")
|
||||||
log.warn "Harmony Access token has expired"
|
log.warn "Harmony - Harmony Access token has expired"
|
||||||
}
|
}
|
||||||
} catch (java.net.SocketTimeoutException e) {
|
} catch (java.net.SocketTimeoutException e) {
|
||||||
log.warn "Connection to the hub timed out. Please restart the hub and try again."
|
log.warn "Harmony - Connection to the hub timed out. Please restart the hub and try again."
|
||||||
state.resethub = true
|
state.resethub = true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.info "Logitech Harmony - Error: $e"
|
log.info "Harmony - Error: $e"
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
def addDevice() {
|
def addDevice() {
|
||||||
log.trace "Adding Hubs"
|
log.trace "Harmony - Adding Hubs"
|
||||||
selectedhubs.each { dni ->
|
selectedhubs.each { dni ->
|
||||||
def d = getChildDevice(dni)
|
def d = getChildDevice(dni)
|
||||||
if(!d) {
|
if(!d) {
|
||||||
def newAction = state.HarmonyHubs.find { it.key == dni }
|
def newAction = state.HarmonyHubs.find { it.key == dni }
|
||||||
d = addChildDevice("smartthings", "Logitech Harmony Hub C2C", dni, null, [label:"${newAction.value}"])
|
d = addChildDevice("smartthings", "Logitech Harmony Hub C2C", dni, null, [label:"${newAction.value}"])
|
||||||
log.trace "created ${d.displayName} with id $dni"
|
log.trace "Harmony - Created ${d.displayName} with id $dni"
|
||||||
poll()
|
poll()
|
||||||
} else {
|
} else {
|
||||||
log.trace "found ${d.displayName} with id $dni already exists"
|
log.trace "Harmony - Found ${d.displayName} with id $dni already exists"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.trace "Adding Activities"
|
log.trace "Harmony - Adding Activities"
|
||||||
selectedactivities.each { dni ->
|
selectedactivities.each { dni ->
|
||||||
def d = getChildDevice(dni)
|
def d = getChildDevice(dni)
|
||||||
if(!d) {
|
if(!d) {
|
||||||
def newAction = state.HarmonyActivities.find { it.key == dni }
|
def newAction = state.HarmonyActivities.find { it.key == dni }
|
||||||
if (newAction) {
|
if (newAction) {
|
||||||
d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"])
|
d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"])
|
||||||
log.trace "created ${d.displayName} with id $dni"
|
log.trace "Harmony - Created ${d.displayName} with id $dni"
|
||||||
poll()
|
poll()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.trace "found ${d.displayName} with id $dni already exists"
|
log.trace "Harmony - Found ${d.displayName} with id $dni already exists"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def activity(dni,mode) {
|
def activity(dni,mode) {
|
||||||
def Params = [auth: state.HarmonyAccessToken]
|
def tokenParam = [auth: state.HarmonyAccessToken]
|
||||||
def msg = "Command failed"
|
def url
|
||||||
def url = ''
|
|
||||||
if (dni == "all") {
|
if (dni == "all") {
|
||||||
url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(Params)}"
|
url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(tokenParam)}"
|
||||||
} else {
|
} else {
|
||||||
def aux = dni.split('-')
|
def aux = dni.split('-')
|
||||||
def hubId = aux[1]
|
def hubId = aux[1]
|
||||||
if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){
|
if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){
|
||||||
url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(Params)}"
|
url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(tokenParam)}"
|
||||||
} else {
|
} else {
|
||||||
def activityId = aux[2]
|
def activityId = aux[2]
|
||||||
url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/${activityId}/${mode}?${toQueryString(Params)}"
|
url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/${activityId}/${mode}?${toQueryString(tokenParam)}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
def params = [
|
||||||
httpPostJson(uri: url) { response ->
|
uri: url,
|
||||||
if (response.data.code == 200 || dni == "all") {
|
contentType: 'application/json'
|
||||||
msg = "Command sent succesfully"
|
]
|
||||||
state.aux = 0
|
asynchttp_v1.post('activityResponse', params)
|
||||||
} else {
|
return "Command Sent"
|
||||||
msg = "Command failed. Error: $response.data.code"
|
}
|
||||||
}
|
|
||||||
}
|
def activityResponse(response, data) {
|
||||||
} catch (groovyx.net.http.HttpResponseException ex) {
|
if (response.hasError()) {
|
||||||
log.error ex
|
log.error "Harmony - response has error: $response.errorMessage"
|
||||||
if (state.aux == 0) {
|
if (response.status == 401) { // token is expired
|
||||||
state.aux = 1
|
state.remove("HarmonyAccessToken")
|
||||||
activity(dni,mode)
|
log.warn "Harmony - Access token has expired"
|
||||||
} else {
|
|
||||||
msg = ex
|
|
||||||
state.aux = 0
|
|
||||||
}
|
|
||||||
} catch(Exception ex) {
|
|
||||||
msg = ex
|
|
||||||
}
|
}
|
||||||
runIn(10, "poll", [overwrite: true])
|
} else {
|
||||||
return msg
|
if (response.status == 200) {
|
||||||
|
log.trace "Harmony - Command sent succesfully"
|
||||||
|
poll()
|
||||||
|
} else {
|
||||||
|
log.trace "Harmony - Command failed. Error: $response.status"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def poll() {
|
def poll() {
|
||||||
// GET THE LIST OF ACTIVITIES
|
// GET THE LIST OF ACTIVITIES
|
||||||
if (state.HarmonyAccessToken) {
|
if (state.HarmonyAccessToken) {
|
||||||
getActivityList()
|
def tokenParam = [auth: state.HarmonyAccessToken]
|
||||||
def Params = [auth: state.HarmonyAccessToken]
|
def params = [
|
||||||
def url = "https://home.myharmony.com/cloudapi/state?${toQueryString(Params)}"
|
uri: "https://home.myharmony.com/cloudapi/state?${toQueryString(tokenParam)}",
|
||||||
try {
|
headers: ["Accept": "application/json"],
|
||||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
contentType: 'application/json'
|
||||||
def map = [:]
|
]
|
||||||
response.data.hubs.each {
|
asynchttp_v1.get('pollResponse', params)
|
||||||
if (it.value.message == "OK") {
|
} else {
|
||||||
map["${it.key}"] = "${it.value.response.data.currentAvActivity},${it.value.response.data.activityStatus}"
|
log.warn "Harmony - Access token has expired"
|
||||||
def hub = getChildDevice("harmony-${it.key}")
|
}
|
||||||
if (hub) {
|
|
||||||
if (it.value.response.data.currentAvActivity == "-1") {
|
|
||||||
hub.sendEvent(name: "currentActivity", value: "--", descriptionText: "There isn't any activity running", display: false)
|
|
||||||
} else {
|
|
||||||
def currentActivity = getActivityName(it.value.response.data.currentAvActivity,it.key)
|
|
||||||
hub.sendEvent(name: "currentActivity", value: currentActivity, descriptionText: "Current activity is ${currentActivity}", display: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.trace it.value.message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
def activities = getChildDevices()
|
|
||||||
def activitynotrunning = true
|
|
||||||
activities.each { activity ->
|
|
||||||
def act = activity.deviceNetworkId.split('-')
|
|
||||||
if (act.size() > 2) {
|
|
||||||
def aux = map.find { it.key == act[1] }
|
|
||||||
if (aux) {
|
|
||||||
def aux2 = aux.value.split(',')
|
|
||||||
def childDevice = getChildDevice(activity.deviceNetworkId)
|
|
||||||
if ((act[2] == aux2[0]) && (aux2[1] == "1" || aux2[1] == "2")) {
|
|
||||||
childDevice?.sendEvent(name: "switch", value: "on")
|
|
||||||
if (aux2[1] == "1")
|
|
||||||
runIn(5, "poll", [overwrite: true])
|
|
||||||
} else {
|
|
||||||
childDevice?.sendEvent(name: "switch", value: "off")
|
|
||||||
if (aux2[1] == "3")
|
|
||||||
runIn(5, "poll", [overwrite: true])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "Poll completed $map - $state.hubs"
|
|
||||||
}
|
|
||||||
} catch (groovyx.net.http.HttpResponseException e) {
|
|
||||||
if (e.statusCode == 401) { // token is expired
|
|
||||||
state.remove("HarmonyAccessToken")
|
|
||||||
log.warn "Harmony Access token has expired"
|
|
||||||
}
|
|
||||||
} catch (java.net.SocketTimeoutException e) {
|
|
||||||
log.warn "Connection to the hub timed out. Please restart the hub and try again."
|
|
||||||
state.resethub = true
|
|
||||||
} catch (e) {
|
|
||||||
log.info "Logitech Harmony - Error: $e"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def pollResponse(response, data) {
|
||||||
|
if (response.hasError()) {
|
||||||
|
log.error "Harmony - response has error: $response.errorMessage"
|
||||||
|
if (response.status == 401) { // token is expired
|
||||||
|
state.remove("HarmonyAccessToken")
|
||||||
|
log.warn "Harmony - Access token has expired"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
def ResponseValues
|
||||||
|
try {
|
||||||
|
// json response already parsed into JSONElement object
|
||||||
|
ResponseValues = response.json
|
||||||
|
} catch (e) {
|
||||||
|
log.error "Harmony - error parsing json from response: $e"
|
||||||
|
}
|
||||||
|
if (ResponseValues) {
|
||||||
|
def map = [:]
|
||||||
|
ResponseValues.hubs.each {
|
||||||
|
if (it.value.message == "OK") {
|
||||||
|
map["${it.key}"] = "${it.value.response.data.currentAvActivity},${it.value.response.data.activityStatus}"
|
||||||
|
def hub = getChildDevice("harmony-${it.key}")
|
||||||
|
if (hub) {
|
||||||
|
if (it.value.response.data.currentAvActivity == "-1") {
|
||||||
|
hub.sendEvent(name: "currentActivity", value: "--", descriptionText: "There isn't any activity running", display: false)
|
||||||
|
} else {
|
||||||
|
def currentActivity
|
||||||
|
def activityDTH = getChildDevice("harmony-${it.key}-${it.value.response.data.currentAvActivity}")
|
||||||
|
if (activityDTH)
|
||||||
|
currentActivity = activityDTH.device.displayName
|
||||||
|
else
|
||||||
|
currentActivity = getActivityName(it.value.response.data.currentAvActivity,it.key)
|
||||||
|
hub.sendEvent(name: "currentActivity", value: currentActivity, descriptionText: "Current activity is ${currentActivity}", display: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.trace "Harmony - error response: $it.value.message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def activities = getChildDevices()
|
||||||
|
def activitynotrunning = true
|
||||||
|
activities.each { activity ->
|
||||||
|
def act = activity.deviceNetworkId.split('-')
|
||||||
|
if (act.size() > 2) {
|
||||||
|
def aux = map.find { it.key == act[1] }
|
||||||
|
if (aux) {
|
||||||
|
def aux2 = aux.value.split(',')
|
||||||
|
def childDevice = getChildDevice(activity.deviceNetworkId)
|
||||||
|
if ((act[2] == aux2[0]) && (aux2[1] == "1" || aux2[1] == "2")) {
|
||||||
|
childDevice?.sendEvent(name: "switch", value: "on")
|
||||||
|
if (aux2[1] == "1")
|
||||||
|
runIn(5, "poll", [overwrite: true])
|
||||||
|
} else {
|
||||||
|
childDevice?.sendEvent(name: "switch", value: "off")
|
||||||
|
if (aux2[1] == "3")
|
||||||
|
runIn(5, "poll", [overwrite: true])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.debug "Harmony - did not get json results from response body: $response.data"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def getActivityList() {
|
def getActivityList() {
|
||||||
// GET ACTIVITY'S NAME
|
|
||||||
if (state.HarmonyAccessToken) {
|
if (state.HarmonyAccessToken) {
|
||||||
def Params = [auth: state.HarmonyAccessToken]
|
def Params = [auth: state.HarmonyAccessToken]
|
||||||
def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}"
|
def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}"
|
||||||
@@ -552,21 +568,19 @@ def getActivityList() {
|
|||||||
[id: it.key, name: it.value['name'], type: it.value['type']]
|
[id: it.key, name: it.value['name'], type: it.value['type']]
|
||||||
}
|
}
|
||||||
activities += [id: "off", name: "Activity OFF", type: "0"]
|
activities += [id: "off", name: "Activity OFF", type: "0"]
|
||||||
log.trace activities
|
|
||||||
}
|
}
|
||||||
hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", display: false)
|
hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", display: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (groovyx.net.http.HttpResponseException e) {
|
} catch (groovyx.net.http.HttpResponseException e) {
|
||||||
log.trace e
|
log.trace e
|
||||||
} catch (java.net.SocketTimeoutException e) {
|
} catch (java.net.SocketTimeoutException e) {
|
||||||
log.trace e
|
log.trace e
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
log.trace e
|
log.trace e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return activity
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def getActivityName(activity,hubId) {
|
def getActivityName(activity,hubId) {
|
||||||
@@ -629,7 +643,7 @@ def sendNotification(msg) {
|
|||||||
|
|
||||||
def hookEventHandler() {
|
def hookEventHandler() {
|
||||||
// log.debug "In hookEventHandler method."
|
// log.debug "In hookEventHandler method."
|
||||||
log.debug "request = ${request}"
|
log.debug "Harmony - request = ${request}"
|
||||||
|
|
||||||
def json = request.JSON
|
def json = request.JSON
|
||||||
|
|
||||||
@@ -638,14 +652,14 @@ def hookEventHandler() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def listDevices() {
|
def listDevices() {
|
||||||
log.debug "getDevices, params: ${params}"
|
log.debug "Harmony - getDevices(), params: ${params}"
|
||||||
allDevices.collect {
|
allDevices.collect {
|
||||||
deviceItem(it)
|
deviceItem(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def getDevice() {
|
def getDevice() {
|
||||||
log.debug "getDevice, params: ${params}"
|
log.debug "Harmony - getDevice(), params: ${params}"
|
||||||
def device = allDevices.find { it.id == params.id }
|
def device = allDevices.find { it.id == params.id }
|
||||||
if (!device) {
|
if (!device) {
|
||||||
render status: 404, data: '{"msg": "Device not found"}'
|
render status: 404, data: '{"msg": "Device not found"}'
|
||||||
@@ -658,7 +672,7 @@ def updateDevice() {
|
|||||||
def data = request.JSON
|
def data = request.JSON
|
||||||
def command = data.command
|
def command = data.command
|
||||||
def arguments = data.arguments
|
def arguments = data.arguments
|
||||||
log.debug "updateDevice, params: ${params}, request: ${data}"
|
log.debug "Harmony - updateDevice(), params: ${params}, request: ${data}"
|
||||||
if (!command) {
|
if (!command) {
|
||||||
render status: 400, data: '{"msg": "command is required"}'
|
render status: 400, data: '{"msg": "command is required"}'
|
||||||
} else {
|
} else {
|
||||||
@@ -726,7 +740,7 @@ def getDeviceCapabilityCommands(deviceCapabilities) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def listSubscriptions() {
|
def listSubscriptions() {
|
||||||
log.debug "listSubscriptions()"
|
log.debug "Harmony - listSubscriptions()"
|
||||||
app.subscriptions?.findAll { it.device?.device && it.device.id }?.collect {
|
app.subscriptions?.findAll { it.device?.device && it.device.id }?.collect {
|
||||||
def deviceInfo = state[it.device.id]
|
def deviceInfo = state[it.device.id]
|
||||||
def response = [
|
def response = [
|
||||||
@@ -747,17 +761,17 @@ def addSubscription() {
|
|||||||
def attribute = data.attributeName
|
def attribute = data.attributeName
|
||||||
def callbackUrl = data.callbackUrl
|
def callbackUrl = data.callbackUrl
|
||||||
|
|
||||||
log.debug "addSubscription, params: ${params}, request: ${data}"
|
log.debug "Harmony - addSubscription, params: ${params}, request: ${data}"
|
||||||
if (!attribute) {
|
if (!attribute) {
|
||||||
render status: 400, data: '{"msg": "attributeName is required"}'
|
render status: 400, data: '{"msg": "attributeName is required"}'
|
||||||
} else {
|
} else {
|
||||||
def device = allDevices.find { it.id == data.deviceId }
|
def device = allDevices.find { it.id == data.deviceId }
|
||||||
if (device) {
|
if (device) {
|
||||||
if (!state.harmonyHubs) {
|
if (!state.harmonyHubs) {
|
||||||
log.debug "Adding callbackUrl: $callbackUrl"
|
log.debug "Harmony - Adding callbackUrl: $callbackUrl"
|
||||||
state[device.id] = [callbackUrl: callbackUrl]
|
state[device.id] = [callbackUrl: callbackUrl]
|
||||||
}
|
}
|
||||||
log.debug "Adding subscription"
|
log.debug "Harmony - Adding subscription"
|
||||||
def subscription = subscribe(device, attribute, deviceHandler)
|
def subscription = subscribe(device, attribute, deviceHandler)
|
||||||
if (!subscription || !subscription.eventSubscription) {
|
if (!subscription || !subscription.eventSubscription) {
|
||||||
subscription = app.subscriptions?.find { it.device?.device && it.device.id == data.deviceId && it.data == attribute && it.handler == 'deviceHandler' }
|
subscription = app.subscriptions?.find { it.device?.device && it.device.id == data.deviceId && it.data == attribute && it.handler == 'deviceHandler' }
|
||||||
@@ -785,7 +799,7 @@ def removeSubscription() {
|
|||||||
|
|
||||||
log.debug "removeSubscription, params: ${params}, subscription: ${subscription}, device: ${device}"
|
log.debug "removeSubscription, params: ${params}, subscription: ${subscription}, device: ${device}"
|
||||||
if (device) {
|
if (device) {
|
||||||
log.debug "Removing subscription for device: ${device.id}"
|
log.debug "Harmony - Removing subscription for device: ${device.id}"
|
||||||
state.remove(device.id)
|
state.remove(device.id)
|
||||||
unsubscribe(device)
|
unsubscribe(device)
|
||||||
}
|
}
|
||||||
@@ -809,16 +823,17 @@ def deviceHandler(evt) {
|
|||||||
def deviceInfo = state[evt.deviceId]
|
def deviceInfo = state[evt.deviceId]
|
||||||
if (state.harmonyHubs) {
|
if (state.harmonyHubs) {
|
||||||
state.harmonyHubs.each { harmonyHub ->
|
state.harmonyHubs.each { harmonyHub ->
|
||||||
|
log.trace "Harmony - Sending data to $harmonyHub.name"
|
||||||
sendToHarmony(evt, harmonyHub.callbackUrl)
|
sendToHarmony(evt, harmonyHub.callbackUrl)
|
||||||
}
|
}
|
||||||
} else if (deviceInfo) {
|
} else if (deviceInfo) {
|
||||||
if (deviceInfo.callbackUrl) {
|
if (deviceInfo.callbackUrl) {
|
||||||
sendToHarmony(evt, deviceInfo.callbackUrl)
|
sendToHarmony(evt, deviceInfo.callbackUrl)
|
||||||
} else {
|
} else {
|
||||||
log.warn "No callbackUrl set for device: ${evt.deviceId}"
|
log.warn "Harmony - No callbackUrl set for device: ${evt.deviceId}"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.warn "No subscribed device found for device: ${evt.deviceId}"
|
log.warn "Harmony - No subscribed device found for device: ${evt.deviceId}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -842,12 +857,12 @@ def sendToHarmony(evt, String callbackUrl) {
|
|||||||
body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]]
|
body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]]
|
||||||
]
|
]
|
||||||
try {
|
try {
|
||||||
log.debug "Sending data to Harmony Cloud: $params"
|
log.debug "Harmony - Sending data to Harmony Cloud: $params"
|
||||||
httpPostJson(params) { resp ->
|
httpPostJson(params) { resp ->
|
||||||
log.debug "Harmony Cloud - Response: ${resp.status}"
|
log.debug "Harmony - Cloud Response: ${resp.status}"
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error "Harmony Cloud - Something went wrong: $e"
|
log.error "Harmony - Cloud Something went wrong: $e"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -872,10 +887,10 @@ def activityCallback() {
|
|||||||
if (data.errorCode == "200") {
|
if (data.errorCode == "200") {
|
||||||
device.setCurrentActivity(data.currentActivityId)
|
device.setCurrentActivity(data.currentActivityId)
|
||||||
} else {
|
} else {
|
||||||
log.warn "Activity callback error: ${data}"
|
log.warn "Harmony - Activity callback error: ${data}"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.warn "Activity callback sent to non-existant dni: ${params.dni}"
|
log.warn "Harmony - Activity callback sent to non-existant dni: ${params.dni}"
|
||||||
}
|
}
|
||||||
render status: 200, data: '{"msg": "Successfully received callbackUrl"}'
|
render status: 200, data: '{"msg": "Successfully received callbackUrl"}'
|
||||||
}
|
}
|
||||||
@@ -909,13 +924,13 @@ def harmony() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def deleteHarmony() {
|
def deleteHarmony() {
|
||||||
log.debug "Trying to delete Harmony hub with mac: ${params.mac}"
|
log.debug "Harmony - Trying to delete Harmony hub with mac: ${params.mac}"
|
||||||
def harmonyHub = state.harmonyHubs?.find { it.mac == params.mac }
|
def harmonyHub = state.harmonyHubs?.find { it.mac == params.mac }
|
||||||
if (harmonyHub) {
|
if (harmonyHub) {
|
||||||
log.debug "Deleting Harmony hub with mac: ${params.mac}"
|
log.debug "Harmony - Deleting Harmony hub with mac: ${params.mac}"
|
||||||
state.harmonyHubs.remove(harmonyHub)
|
state.harmonyHubs.remove(harmonyHub)
|
||||||
} else {
|
} else {
|
||||||
log.debug "Couldn't find Harmony hub with mac: ${params.mac}"
|
log.debug "Harmony - Couldn't find Harmony hub with mac: ${params.mac}"
|
||||||
}
|
}
|
||||||
render status: 204, data: "{}"
|
render status: 204, data: "{}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,10 +41,10 @@ def updated() {
|
|||||||
|
|
||||||
def presenceHandler(evt) {
|
def presenceHandler(evt) {
|
||||||
if (evt.value == "present") {
|
if (evt.value == "present") {
|
||||||
log.debug "${presence.label ?: presence.name} has arrived at the ${location}"
|
// log.debug "${presence.label ?: presence.name} has arrived at the ${location}"
|
||||||
sendPush("${presence.label ?: presence.name} has arrived at the ${location}")
|
sendPush("${presence.label ?: presence.name} has arrived at the ${location}")
|
||||||
} else if (evt.value == "not present") {
|
} else if (evt.value == "not present") {
|
||||||
log.debug "${presence.label ?: presence.name} has left the ${location}"
|
// log.debug "${presence.label ?: presence.name} has left the ${location}"
|
||||||
sendPush("${presence.label ?: presence.name} has left the ${location}")
|
sendPush("${presence.label ?: presence.name} has left the ${location}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ def updated() {
|
|||||||
|
|
||||||
def presenceHandler(evt) {
|
def presenceHandler(evt) {
|
||||||
if (evt.value == "present") {
|
if (evt.value == "present") {
|
||||||
log.debug "${presence.label ?: presence.name} has arrived at the ${location}"
|
// log.debug "${presence.label ?: presence.name} has arrived at the ${location}"
|
||||||
|
|
||||||
if (location.contactBookEnabled) {
|
if (location.contactBookEnabled) {
|
||||||
sendNotificationToContacts("${presence.label ?: presence.name} has arrived at the ${location}", recipients)
|
sendNotificationToContacts("${presence.label ?: presence.name} has arrived at the ${location}", recipients)
|
||||||
@@ -56,7 +56,7 @@ def presenceHandler(evt) {
|
|||||||
sendSms(phone1, "${presence.label ?: presence.name} has arrived at the ${location}")
|
sendSms(phone1, "${presence.label ?: presence.name} has arrived at the ${location}")
|
||||||
}
|
}
|
||||||
} else if (evt.value == "not present") {
|
} else if (evt.value == "not present") {
|
||||||
log.debug "${presence.label ?: presence.name} has left the ${location}"
|
// log.debug "${presence.label ?: presence.name} has left the ${location}"
|
||||||
|
|
||||||
if (location.contactBookEnabled) {
|
if (location.contactBookEnabled) {
|
||||||
sendNotificationToContacts("${presence.label ?: presence.name} has left the ${location}", recipients)
|
sendNotificationToContacts("${presence.label ?: presence.name} has left the ${location}", recipients)
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ def updated() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def subscribe() {
|
def subscribe() {
|
||||||
log.debug "present: ${cars.collect{it.displayName + ': ' + it.currentPresence}}"
|
// log.debug "present: ${cars.collect{it.displayName + ': ' + it.currentPresence}}"
|
||||||
subscribe(doorSensor, "contact", garageDoorContact)
|
subscribe(doorSensor, "contact", garageDoorContact)
|
||||||
|
|
||||||
subscribe(cars, "presence", carPresence)
|
subscribe(cars, "presence", carPresence)
|
||||||
|
|||||||
@@ -26,17 +26,22 @@ definition(
|
|||||||
)
|
)
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
section ("In addition to push notifications, send text alerts to...") {
|
|
||||||
input("recipients", "contact", title: "Send notifications to") {
|
if (!(location.zipCode || ( location.latitude && location.longitude )) && location.channelName == 'samsungtv') {
|
||||||
input "phone1", "phone", title: "Phone Number 1", required: false
|
section { paragraph title: "Note:", "Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location." }
|
||||||
input "phone2", "phone", title: "Phone Number 2", required: false
|
|
||||||
input "phone3", "phone", title: "Phone Number 3", required: false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
section ("Zip code (optional, defaults to location coordinates)...") {
|
if (location.channelName != 'samsungtv') {
|
||||||
input "zipcode", "text", title: "Zip Code", required: false
|
section( "Set your location" ) { input "zipCode", "text", title: "Zip code" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
section ("In addition to push notifications, send text alerts to...") {
|
||||||
|
input("recipients", "contact", title: "Send notifications to") {
|
||||||
|
input "phone1", "phone", title: "Phone Number 1", required: false
|
||||||
|
input "phone2", "phone", title: "Phone Number 2", required: false
|
||||||
|
input "phone3", "phone", title: "Phone Number 3", required: false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def installed() {
|
def installed() {
|
||||||
@@ -61,7 +66,7 @@ def checkForSevereWeather() {
|
|||||||
def alerts
|
def alerts
|
||||||
if(locationIsDefined()) {
|
if(locationIsDefined()) {
|
||||||
if(zipcodeIsValid()) {
|
if(zipcodeIsValid()) {
|
||||||
alerts = getWeatherFeature("alerts", zipcode)?.alerts
|
alerts = getWeatherFeature("alerts", zipCode)?.alerts
|
||||||
} else {
|
} else {
|
||||||
log.warn "Severe Weather Alert: Invalid zipcode entered, defaulting to location's zipcode"
|
log.warn "Severe Weather Alert: Invalid zipcode entered, defaulting to location's zipcode"
|
||||||
alerts = getWeatherFeature("alerts")?.alerts
|
alerts = getWeatherFeature("alerts")?.alerts
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user