mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-15 13:10:51 +00:00
Compare commits
92 Commits
MSA-628-2
...
master_old
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01a36696d8 | ||
|
|
797def2935 | ||
|
|
1034cd06e6 | ||
|
|
c37729242e | ||
|
|
d29c3ec557 | ||
|
|
17be85b846 | ||
|
|
da06104563 | ||
|
|
5d37ac8515 | ||
|
|
2a739fda07 | ||
|
|
f627fb4fac | ||
|
|
1d30a718b2 | ||
|
|
303ca7117c | ||
|
|
1c96645b9f | ||
|
|
af4dc0640a | ||
|
|
80b46153dc | ||
|
|
b92cd9c637 | ||
|
|
1039b65c81 | ||
|
|
4e203e8e13 | ||
|
|
61398105d1 | ||
|
|
505efc5463 | ||
|
|
3ddc82f996 | ||
|
|
13a324069d | ||
|
|
2fd5859326 | ||
|
|
bbedbddf9d | ||
|
|
e424e7abdd | ||
|
|
47fbdabf6b | ||
|
|
7defe1cc61 | ||
|
|
a78459347b | ||
|
|
c473745e47 | ||
|
|
fc587ef15a | ||
|
|
0f3b730f26 | ||
|
|
11df2f31b3 | ||
|
|
5e93bca030 | ||
|
|
8b6942525d | ||
|
|
1a8eab065b | ||
|
|
74e3e9f70d | ||
|
|
738d41f5af | ||
|
|
cf69d6127d | ||
|
|
ef4a32ac2e | ||
|
|
ac7592d132 | ||
|
|
a0a3cd7cf9 | ||
|
|
14399f8fc8 | ||
|
|
1f76b9e960 | ||
|
|
5d1630b367 | ||
|
|
0af48a5caf | ||
|
|
a5aeeed77f | ||
|
|
0445b415f7 | ||
|
|
303219fcc5 | ||
|
|
4e88a3ac60 | ||
|
|
9b8be908e2 | ||
|
|
ea65ed58dc | ||
|
|
d19ec0b525 | ||
|
|
9654c27ca8 | ||
|
|
dec9ff20b0 | ||
|
|
c62fd52a26 | ||
|
|
1195161232 | ||
|
|
5f1ff8a5c6 | ||
|
|
7aabd9bc5f | ||
|
|
f260e36d54 | ||
|
|
594b61c852 | ||
|
|
c416560f19 | ||
|
|
1ee768016f | ||
|
|
54e5334cca | ||
|
|
8c55b6314a | ||
|
|
ac5f15efd8 | ||
|
|
bbc680746e | ||
|
|
702f0f87d0 | ||
|
|
d830c1fae0 | ||
|
|
f337e8a085 | ||
|
|
8f6201507a | ||
|
|
0171644a77 | ||
|
|
58187006b7 | ||
|
|
5e6bfb5857 | ||
|
|
2d22b5a384 | ||
|
|
10dee44c0a | ||
|
|
20e5b51aef | ||
|
|
20f1a76889 | ||
|
|
6db15b12c1 | ||
|
|
aa93850c62 | ||
|
|
47210ca8b4 | ||
|
|
cbd5c91d52 | ||
|
|
4d243bf44d | ||
|
|
794ff6b68a | ||
|
|
ef21fd4257 | ||
|
|
1965f10584 | ||
|
|
9c9fba0939 | ||
|
|
725f9ebec7 | ||
|
|
e217805d98 | ||
|
|
ff39270ba4 | ||
|
|
9d016839c8 | ||
|
|
ecb975540b | ||
|
|
8ba5eaf74d |
@@ -1,12 +1,9 @@
|
|||||||
/**
|
// keen home smart vent
|
||||||
* Keen Home Smart Vent
|
// http://www.keenhome.io
|
||||||
*
|
// SmartThings Device Handler v1.0.0
|
||||||
* Author: Keen Home
|
|
||||||
* Date: 2015-06-23
|
|
||||||
*/
|
|
||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "Keen Home Smart Vent", namespace: "Keen Home", author: "Gregg Altschul") {
|
definition (name: "Keen Home Smart Vent", namespace: "Keen Home", author: "Keen Home") {
|
||||||
capability "Switch Level"
|
capability "Switch Level"
|
||||||
capability "Switch"
|
capability "Switch"
|
||||||
capability "Configuration"
|
capability "Configuration"
|
||||||
@@ -21,6 +18,7 @@ metadata {
|
|||||||
command "getBattery"
|
command "getBattery"
|
||||||
command "getTemperature"
|
command "getTemperature"
|
||||||
command "setZigBeeIdTile"
|
command "setZigBeeIdTile"
|
||||||
|
command "clearObstruction"
|
||||||
|
|
||||||
fingerprint endpoint: "1",
|
fingerprint endpoint: "1",
|
||||||
profileId: "0104",
|
profileId: "0104",
|
||||||
@@ -42,9 +40,10 @@ metadata {
|
|||||||
// UI tile definitions
|
// UI tile definitions
|
||||||
tiles {
|
tiles {
|
||||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
state "on", action:"switch.off", icon:"st.vents.vent-open-text", backgroundColor:"#53a7c0"
|
state "on", action: "switch.off", icon: "st.vents.vent-open-text", backgroundColor: "#53a7c0"
|
||||||
state "off", action:"switch.on", icon:"st.vents.vent-closed", backgroundColor:"#ffffff"
|
state "off", action: "switch.on", icon: "st.vents.vent-closed", backgroundColor: "#ffffff"
|
||||||
state "obstructed", action: "switch.off", icon:"st.vents.vent-closed", backgroundColor:"#ff0000"
|
state "obstructed", action: "clearObstruction", icon: "st.vents.vent-closed", backgroundColor: "#ff0000"
|
||||||
|
state "clearing", action: "", icon: "st.vents.vent-closed", backgroundColor: "#ffff33"
|
||||||
}
|
}
|
||||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) {
|
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||||
state "level", action:"switch level.setLevel"
|
state "level", action:"switch level.setLevel"
|
||||||
@@ -206,12 +205,12 @@ private Map makeOnOffResult(rawValue) {
|
|||||||
|
|
||||||
private Map makeLevelResult(rawValue) {
|
private Map makeLevelResult(rawValue) {
|
||||||
def linkText = getLinkText(device)
|
def linkText = getLinkText(device)
|
||||||
// log.debug "rawValue: ${rawValue}"
|
|
||||||
def value = Integer.parseInt(rawValue, 16)
|
def value = Integer.parseInt(rawValue, 16)
|
||||||
def rangeMax = 254
|
def rangeMax = 254
|
||||||
|
|
||||||
|
// catch obstruction level
|
||||||
if (value == 255) {
|
if (value == 255) {
|
||||||
log.debug "obstructed"
|
log.debug "${linkText} is obstructed"
|
||||||
// Just return here. Once the vent is power cycled
|
// Just return here. Once the vent is power cycled
|
||||||
// it will go back to the previous level before obstruction.
|
// it will go back to the previous level before obstruction.
|
||||||
// Therefore, no need to update level on the display.
|
// Therefore, no need to update level on the display.
|
||||||
@@ -220,24 +219,9 @@ private Map makeLevelResult(rawValue) {
|
|||||||
value: "obstructed",
|
value: "obstructed",
|
||||||
descriptionText: "${linkText} is obstructed. Please power cycle."
|
descriptionText: "${linkText} is obstructed. Please power cycle."
|
||||||
]
|
]
|
||||||
} else if ( device.currentValue("switch") == "obstructed" &&
|
|
||||||
value == 254) {
|
|
||||||
// When the device is reset after an obstruction, the switch
|
|
||||||
// state will be obstructed and the value coming from the device
|
|
||||||
// will be 254. Since we're not using heating/cooling mode from
|
|
||||||
// the device type handler, we need to bump it down to the lower
|
|
||||||
// (cooling) range
|
|
||||||
sendEvent(makeOnOffResult(1)) // clear the obstructed switch state
|
|
||||||
value = rangeMax
|
|
||||||
}
|
}
|
||||||
// else if (device.currentValue("switch") == "off") {
|
|
||||||
// sendEvent(makeOnOffResult(1)) // turn back on if in off state
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
// log.debug "pre-value: ${value}"
|
|
||||||
value = Math.floor(value / rangeMax * 100)
|
value = Math.floor(value / rangeMax * 100)
|
||||||
// log.debug "post-value: ${value}"
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
name: "level",
|
name: "level",
|
||||||
@@ -327,35 +311,79 @@ private def makeSerialResult(serial) {
|
|||||||
value: serial,
|
value: serial,
|
||||||
descriptionText: "${linkText} has serial ${serial}" ]
|
descriptionText: "${linkText} has serial ${serial}" ]
|
||||||
}
|
}
|
||||||
/**** COMMAND METHODS ****/
|
|
||||||
// def mfgCode() {
|
|
||||||
// ["zcl mfg-code 0x115B", "delay 200"]
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
// takes a level from 0 to 100 and translates it to a ZigBee move to level with on/off command
|
||||||
|
private def makeLevelCommand(level) {
|
||||||
|
def rangeMax = 254
|
||||||
|
def scaledLevel = Math.round(level * rangeMax / 100)
|
||||||
|
log.debug "scaled level for ${level}%: ${scaledLevel}"
|
||||||
|
|
||||||
|
// convert to hex string and pad to two digits
|
||||||
|
def hexLevel = new BigInteger(scaledLevel.toString()).toString(16).padLeft(2, '0')
|
||||||
|
|
||||||
|
"st cmd 0x${device.deviceNetworkId} 1 8 4 {${hexLevel} 0000}"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**** COMMAND METHODS ****/
|
||||||
def on() {
|
def on() {
|
||||||
log.debug "on()"
|
def linkText = getLinkText(device)
|
||||||
|
log.debug "open ${linkText}"
|
||||||
|
|
||||||
|
// only change the state if the vent is not obstructed
|
||||||
|
if (device.currentValue("switch") == "obstructed") {
|
||||||
|
log.error("cannot open because ${linkText} is obstructed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
sendEvent(makeOnOffResult(1))
|
sendEvent(makeOnOffResult(1))
|
||||||
"st cmd 0x${device.deviceNetworkId} 1 6 1 {}"
|
"st cmd 0x${device.deviceNetworkId} 1 6 1 {}"
|
||||||
}
|
}
|
||||||
|
|
||||||
def off() {
|
def off() {
|
||||||
log.debug "off()"
|
def linkText = getLinkText(device)
|
||||||
|
log.debug "close ${linkText}"
|
||||||
|
|
||||||
|
// only change the state if the vent is not obstructed
|
||||||
|
if (device.currentValue("switch") == "obstructed") {
|
||||||
|
log.error("cannot close because ${linkText} is obstructed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
sendEvent(makeOnOffResult(0))
|
sendEvent(makeOnOffResult(0))
|
||||||
"st cmd 0x${device.deviceNetworkId} 1 6 0 {}"
|
"st cmd 0x${device.deviceNetworkId} 1 6 0 {}"
|
||||||
}
|
}
|
||||||
|
|
||||||
// does this work?
|
def clearObstruction() {
|
||||||
def toggle() {
|
def linkText = getLinkText(device)
|
||||||
log.debug "toggle()"
|
log.debug "attempting to clear ${linkText} obstruction"
|
||||||
|
|
||||||
"st cmd 0x${device.deviceNetworkId} 1 6 2 {}"
|
sendEvent([
|
||||||
|
name: "switch",
|
||||||
|
value: "clearing",
|
||||||
|
descriptionText: "${linkText} is clearing obstruction"
|
||||||
|
])
|
||||||
|
|
||||||
|
// send a move command to ensure level attribute gets reset for old, buggy firmware
|
||||||
|
// then send a reset to factory defaults
|
||||||
|
// finally re-configure to ensure reports and binding is still properly set after the rtfd
|
||||||
|
[
|
||||||
|
makeLevelCommand(device.currentValue("level")), "delay 500",
|
||||||
|
"st cmd 0x${device.deviceNetworkId} 1 0 0 {}", "delay 5000"
|
||||||
|
] + configure()
|
||||||
}
|
}
|
||||||
|
|
||||||
def setLevel(value) {
|
def setLevel(value) {
|
||||||
log.debug "setting level: ${value}"
|
log.debug "setting level: ${value}"
|
||||||
|
|
||||||
def linkText = getLinkText(device)
|
def linkText = getLinkText(device)
|
||||||
|
|
||||||
|
// only change the level if the vent is not obstructed
|
||||||
|
def currentState = device.currentValue("switch")
|
||||||
|
|
||||||
|
if (currentState == "obstructed") {
|
||||||
|
log.error("cannot set level because ${linkText} is obstructed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
sendEvent(name: "level", value: value)
|
sendEvent(name: "level", value: value)
|
||||||
if (value > 0) {
|
if (value > 0) {
|
||||||
sendEvent(name: "switch", value: "on", descriptionText: "${linkText} is on by setting a level")
|
sendEvent(name: "switch", value: "on", descriptionText: "${linkText} is on by setting a level")
|
||||||
@@ -363,29 +391,26 @@ def setLevel(value) {
|
|||||||
else {
|
else {
|
||||||
sendEvent(name: "switch", value: "off", descriptionText: "${linkText} is off by setting level to 0")
|
sendEvent(name: "switch", value: "off", descriptionText: "${linkText} is off by setting level to 0")
|
||||||
}
|
}
|
||||||
def rangeMax = 254
|
|
||||||
def computedLevel = Math.round(value * rangeMax / 100)
|
|
||||||
log.debug "computedLevel: ${computedLevel}"
|
|
||||||
|
|
||||||
def level = new BigInteger(computedLevel.toString()).toString(16)
|
makeLevelCommand(value)
|
||||||
log.debug "level: ${level}"
|
|
||||||
|
|
||||||
if (level.size() < 2){
|
|
||||||
level = '0' + level
|
|
||||||
}
|
|
||||||
|
|
||||||
"st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} 0000}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def getOnOff() {
|
def getOnOff() {
|
||||||
log.debug "getOnOff()"
|
log.debug "getOnOff()"
|
||||||
|
|
||||||
|
// disallow on/off updates while vent is obstructed
|
||||||
|
if (device.currentValue("switch") == "obstructed") {
|
||||||
|
log.error("cannot update open/close status because ${getLinkText(device)} is obstructed")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
["st rattr 0x${device.deviceNetworkId} 1 0x0006 0"]
|
["st rattr 0x${device.deviceNetworkId} 1 0x0006 0"]
|
||||||
}
|
}
|
||||||
|
|
||||||
def getPressure() {
|
def getPressure() {
|
||||||
log.debug "getPressure()"
|
log.debug "getPressure()"
|
||||||
|
|
||||||
|
// using a Keen Home specific attribute in the pressure measurement cluster
|
||||||
[
|
[
|
||||||
"zcl mfg-code 0x115B", "delay 200",
|
"zcl mfg-code 0x115B", "delay 200",
|
||||||
"zcl global read 0x0403 0x20", "delay 200",
|
"zcl global read 0x0403 0x20", "delay 200",
|
||||||
@@ -395,12 +420,13 @@ def getPressure() {
|
|||||||
|
|
||||||
def getLevel() {
|
def getLevel() {
|
||||||
log.debug "getLevel()"
|
log.debug "getLevel()"
|
||||||
// rattr = read attribute
|
|
||||||
// 0x${} = device net id
|
// disallow level updates while vent is obstructed
|
||||||
// 1 = endpoint
|
if (device.currentValue("switch") == "obstructed") {
|
||||||
// 8 = cluster id (level control, in this case)
|
log.error("cannot update level status because ${getLinkText(device)} is obstructed")
|
||||||
// 0 = attribute within cluster
|
return []
|
||||||
// sendEvent(name: "level", value: value)
|
}
|
||||||
|
|
||||||
["st rattr 0x${device.deviceNetworkId} 1 0x0008 0x0000"]
|
["st rattr 0x${device.deviceNetworkId} 1 0x0008 0x0000"]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,78 +451,59 @@ def setZigBeeIdTile() {
|
|||||||
name: "zigbeeId",
|
name: "zigbeeId",
|
||||||
value: device.zigbeeId,
|
value: device.zigbeeId,
|
||||||
descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ])
|
descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ])
|
||||||
return [
|
return [
|
||||||
name: "zigbeeId",
|
name: "zigbeeId",
|
||||||
value: device.zigbeeId,
|
value: device.zigbeeId,
|
||||||
descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ]
|
descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
getOnOff() +
|
getOnOff() +
|
||||||
getLevel() +
|
getLevel() +
|
||||||
getTemperature() +
|
getTemperature() +
|
||||||
getPressure() +
|
getPressure() +
|
||||||
getBattery()
|
getBattery()
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] reverseArray(byte[] array) {
|
|
||||||
int i = 0;
|
|
||||||
int j = array.length - 1;
|
|
||||||
byte tmp;
|
|
||||||
while (j > i) {
|
|
||||||
tmp = array[j];
|
|
||||||
array[j] = array[i];
|
|
||||||
array[i] = tmp;
|
|
||||||
j--;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
return array
|
|
||||||
}
|
|
||||||
|
|
||||||
private String swapEndianHex(String hex) {
|
|
||||||
reverseArray(hex.decodeHex()).encodeHex()
|
|
||||||
}
|
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
log.debug "CONFIGURE"
|
log.debug "CONFIGURE"
|
||||||
log.debug "zigbeeId: ${device.hub.zigbeeId}"
|
|
||||||
|
|
||||||
|
// get ZigBee ID by hidden tile because that's the only way we can do it
|
||||||
setZigBeeIdTile()
|
setZigBeeIdTile()
|
||||||
|
|
||||||
def configCmds = [
|
def configCmds = [
|
||||||
// binding commands
|
// bind reporting clusters to hub
|
||||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0006 {${device.zigbeeId}} {}", "delay 500",
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0006 {${device.zigbeeId}} {}", "delay 500",
|
||||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0008 {${device.zigbeeId}} {}", "delay 500",
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0008 {${device.zigbeeId}} {}", "delay 500",
|
||||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0402 {${device.zigbeeId}} {}", "delay 500",
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0402 {${device.zigbeeId}} {}", "delay 500",
|
||||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0403 {${device.zigbeeId}} {}", "delay 500",
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0403 {${device.zigbeeId}} {}", "delay 500",
|
||||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0001 {${device.zigbeeId}} {}", "delay 500",
|
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0001 {${device.zigbeeId}} {}", "delay 500"
|
||||||
|
|
||||||
// configure report commands
|
|
||||||
// [cluster] [attr] [type] [min-interval] [max-interval] [min-change]
|
|
||||||
|
|
||||||
// mike 2015/06/22: preconfigured; see tech spec
|
// configure report commands
|
||||||
|
// zcl global send-me-a-report [cluster] [attr] [type] [min-interval] [max-interval] [min-change]
|
||||||
|
|
||||||
|
// report with these parameters is preconfigured in firmware, can be overridden here
|
||||||
// vent on/off state - type: boolean, change: 1
|
// vent on/off state - type: boolean, change: 1
|
||||||
// "zcl global send-me-a-report 6 0 0x10 5 60 {01}", "delay 200",
|
// "zcl global send-me-a-report 6 0 0x10 5 60 {01}", "delay 200",
|
||||||
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
// mike 2015/06/22: preconfigured; see tech spec
|
// report with these parameters is preconfigured in firmware, can be overridden here
|
||||||
// vent level - type: int8u, change: 1
|
// vent level - type: int8u, change: 1
|
||||||
// "zcl global send-me-a-report 8 0 0x20 5 60 {01}", "delay 200",
|
// "zcl global send-me-a-report 8 0 0x20 5 60 {01}", "delay 200",
|
||||||
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
// mike 2015/06/22: temp and pressure reports are preconfigured, but
|
// report with these parameters is preconfigured in firmware, can be overridden here
|
||||||
// we'd like to override their settings for our own purposes
|
|
||||||
// temperature - type: int16s, change: 0xA = 10 = 0.1C
|
// temperature - type: int16s, change: 0xA = 10 = 0.1C
|
||||||
"zcl global send-me-a-report 0x0402 0 0x29 10 60 {0A00}", "delay 200",
|
// "zcl global send-me-a-report 0x0402 0 0x29 60 60 {0A00}", "delay 200",
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
// mike 2015/06/22: use new custom pressure attribute
|
// report with these parameters is preconfigured in firmware, can be overridden here
|
||||||
// pressure - type: int32u, change: 1 = 0.1Pa
|
// keen home custom pressure (tenths of Pascals) - type: int32u, change: 1 = 0.1Pa
|
||||||
"zcl mfg-code 0x115B", "delay 200",
|
// "zcl mfg-code 0x115B", "delay 200",
|
||||||
"zcl global send-me-a-report 0x0403 0x20 0x22 10 60 {010000}", "delay 200",
|
// "zcl global send-me-a-report 0x0403 0x20 0x22 60 60 {010000}", "delay 200",
|
||||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500"
|
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|
||||||
// mike 2015/06/22: preconfigured; see tech spec
|
// report with these parameters is preconfigured in firmware, can be overridden here
|
||||||
// battery - type: int8u, change: 1
|
// battery - type: int8u, change: 1
|
||||||
// "zcl global send-me-a-report 1 0x21 0x20 60 3600 {01}", "delay 200",
|
// "zcl global send-me-a-report 1 0x21 0x20 60 3600 {01}", "delay 200",
|
||||||
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ metadata {
|
|||||||
capability "Battery"
|
capability "Battery"
|
||||||
|
|
||||||
attribute "tamper", "enum", ["detected", "clear"]
|
attribute "tamper", "enum", ["detected", "clear"]
|
||||||
|
attribute "batteryStatus", "string"
|
||||||
|
attribute "powerSupply", "enum", ["USB Cable", "Battery"]
|
||||||
|
|
||||||
fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A", outClusters: "0x5A"
|
fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A", outClusters: "0x5A"
|
||||||
}
|
}
|
||||||
@@ -63,6 +65,19 @@ metadata {
|
|||||||
status "wake up" : "command: 8407, payload: "
|
status "wake up" : "command: 8407, payload: "
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preferences {
|
||||||
|
input description: "Please consult AEOTEC MULTISENSOR 6 operating manual for advanced setting options. You can skip this configuration to use default settings",
|
||||||
|
title: "Advanced Configuration", displayDuringSetup: true, type: "paragraph", element: "paragraph"
|
||||||
|
|
||||||
|
input "motionDelayTime", "enum", title: "Motion Sensor Delay Time",
|
||||||
|
options: ["20 seconds", "40 seconds", "1 minute", "2 minutes", "3 minutes", "4 minutes"], defaultValue: "${motionDelayTime}", displayDuringSetup: true
|
||||||
|
|
||||||
|
input "motionSensitivity", "enum", title: "Motion Sensor Sensitivity", options: ["normal","maximum","minimum"], defaultValue: "${motionSensitivity}", displayDuringSetup: true
|
||||||
|
|
||||||
|
input "reportInterval", "enum", title: "Sensors Report Interval",
|
||||||
|
options: ["8 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${reportInterval}", displayDuringSetup: true
|
||||||
|
}
|
||||||
|
|
||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
multiAttributeTile(name:"motion", type: "generic", width: 6, height: 4){
|
multiAttributeTile(name:"motion", type: "generic", width: 6, height: 4){
|
||||||
tileAttribute ("device.motion", key: "PRIMARY_CONTROL") {
|
tileAttribute ("device.motion", key: "PRIMARY_CONTROL") {
|
||||||
@@ -85,53 +100,78 @@ metadata {
|
|||||||
valueTile("humidity", "device.humidity", inactiveLabel: false, width: 2, height: 2) {
|
valueTile("humidity", "device.humidity", inactiveLabel: false, width: 2, height: 2) {
|
||||||
state "humidity", label:'${currentValue}% humidity', unit:""
|
state "humidity", label:'${currentValue}% humidity', unit:""
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) {
|
valueTile("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) {
|
||||||
state "luminosity", label:'${currentValue} ${unit}', unit:"lux"
|
state "illuminance", label:'${currentValue} ${unit}', unit:"lux"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
valueTile("ultravioletIndex", "device.ultravioletIndex", inactiveLabel: false, width: 2, height: 2) {
|
||||||
|
state "ultravioletIndex", label:'${currentValue} UV index', unit:""
|
||||||
|
}
|
||||||
|
|
||||||
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||||
state "battery", label:'${currentValue}% battery', unit:""
|
state "battery", label:'${currentValue}% battery', unit:""
|
||||||
}
|
}
|
||||||
|
|
||||||
main(["motion", "temperature", "humidity", "illuminance"])
|
valueTile("batteryStatus", "device.batteryStatus", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||||
details(["motion", "temperature", "humidity", "illuminance", "battery"])
|
state "batteryStatus", label:'${currentValue}', unit:""
|
||||||
|
}
|
||||||
|
|
||||||
|
valueTile("powerSupply", "device.powerSupply", height: 2, width: 2, decoration: "flat") {
|
||||||
|
state "powerSupply", label:'${currentValue} powered', backgroundColor:"#ffffff"
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["motion", "temperature", "humidity", "illuminance", "ultravioletIndex"])
|
||||||
|
details(["motion", "temperature", "humidity", "illuminance", "ultravioletIndex", "batteryStatus"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def updated()
|
def updated() {
|
||||||
{
|
log.debug "Updated with settings: ${settings}"
|
||||||
if (state.sec && !isConfigured()) {
|
log.debug "${device.displayName} is now ${device.latestValue("powerSupply")}"
|
||||||
// in case we miss the SCSR
|
|
||||||
|
if (device.latestValue("powerSupply") == "USB Cable") { //case1: USB powered
|
||||||
response(configure())
|
response(configure())
|
||||||
|
} else if (device.latestValue("powerSupply") == "Battery") { //case2: battery powered
|
||||||
|
// setConfigured("false") is used by WakeUpNotification
|
||||||
|
setConfigured("false") //wait until the next time device wakeup to send configure command after user change preference
|
||||||
|
} else { //case3: power source is not identified, ask user to properly pair the sensor again
|
||||||
|
log.warn "power source is not identified, check it sensor is powered by USB, if so > configure()"
|
||||||
|
def request = []
|
||||||
|
request << zwave.configurationV1.configurationGet(parameterNumber: 101)
|
||||||
|
response(commands(request))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse(String description)
|
def parse(String description) {
|
||||||
{
|
log.debug "parse() >> description: $description"
|
||||||
def result = null
|
def result = null
|
||||||
if (description.startsWith("Err 106")) {
|
if (description.startsWith("Err 106")) {
|
||||||
state.sec = 0
|
log.debug "parse() >> Err 106"
|
||||||
result = createEvent( name: "secureInclusion", value: "failed", isStateChange: true,
|
result = createEvent( name: "secureInclusion", value: "failed", isStateChange: true,
|
||||||
descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.")
|
descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.")
|
||||||
} else if (description != "updated") {
|
} else if (description != "updated") {
|
||||||
|
log.debug "parse() >> zwave.parse(description)"
|
||||||
def cmd = zwave.parse(description, [0x31: 5, 0x30: 2, 0x84: 1])
|
def cmd = zwave.parse(description, [0x31: 5, 0x30: 2, 0x84: 1])
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
result = zwaveEvent(cmd)
|
result = zwaveEvent(cmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.debug "Parsed '${description}' to ${result.inspect()}"
|
log.debug "After zwaveEvent(cmd) >> Parsed '${description}' to ${result.inspect()}"
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd)
|
//this notification will be sent only when device is battery powered
|
||||||
{
|
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) {
|
||||||
def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)]
|
def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)]
|
||||||
|
def cmds = []
|
||||||
if (!isConfigured()) {
|
if (!isConfigured()) {
|
||||||
// we're still in the process of configuring a newly joined device
|
|
||||||
log.debug("late configure")
|
log.debug("late configure")
|
||||||
result += response(configure())
|
result << response(configure())
|
||||||
} else {
|
} else {
|
||||||
result += response(zwave.wakeUpV1.wakeUpNoMoreInformation())
|
log.debug("Device has been configured sending >> wakeUpNoMoreInformation()")
|
||||||
|
cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format()
|
||||||
|
result << response(cmds)
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
@@ -149,10 +189,29 @@ def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulat
|
|||||||
}
|
}
|
||||||
|
|
||||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) {
|
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) {
|
||||||
response(configure())
|
log.info "Executing zwaveEvent 98 (SecurityV1): 03 (SecurityCommandsSupportedReport) with cmd: $cmd"
|
||||||
|
state.sec = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) {
|
||||||
|
state.sec = 1
|
||||||
|
log.info "Executing zwaveEvent 98 (SecurityV1): 07 (NetworkKeyVerify) with cmd: $cmd (node is securely included)"
|
||||||
|
def result = [createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful", isStateChange: true)]
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
|
||||||
|
log.info "Executing zwaveEvent 72 (ManufacturerSpecificV2) : 05 (ManufacturerSpecificReport) with cmd: $cmd"
|
||||||
|
log.debug "manufacturerId: ${cmd.manufacturerId}"
|
||||||
|
log.debug "manufacturerName: ${cmd.manufacturerName}"
|
||||||
|
log.debug "productId: ${cmd.productId}"
|
||||||
|
log.debug "productTypeId: ${cmd.productTypeId}"
|
||||||
|
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
|
||||||
|
updateDataValue("MSR", msr)
|
||||||
}
|
}
|
||||||
|
|
||||||
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||||
|
def result = []
|
||||||
def map = [ name: "battery", unit: "%" ]
|
def map = [ name: "battery", unit: "%" ]
|
||||||
if (cmd.batteryLevel == 0xFF) {
|
if (cmd.batteryLevel == 0xFF) {
|
||||||
map.value = 1
|
map.value = 1
|
||||||
@@ -162,11 +221,14 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
|||||||
map.value = cmd.batteryLevel
|
map.value = cmd.batteryLevel
|
||||||
}
|
}
|
||||||
state.lastbatt = now()
|
state.lastbatt = now()
|
||||||
createEvent(map)
|
result << createEvent(map)
|
||||||
|
if (device.latestValue("powerSupply") != "USB Cable"){
|
||||||
|
result << createEvent(name: "batteryStatus", value: "${map.value} % battery", displayed: false)
|
||||||
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd)
|
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd){
|
||||||
{
|
|
||||||
def map = [:]
|
def map = [:]
|
||||||
switch (cmd.sensorType) {
|
switch (cmd.sensorType) {
|
||||||
case 1:
|
case 1:
|
||||||
@@ -208,7 +270,6 @@ def motionEvent(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) {
|
def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) {
|
||||||
setConfigured()
|
|
||||||
motionEvent(cmd.sensorValue)
|
motionEvent(cmd.sensorValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,47 +286,112 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm
|
|||||||
result << createEvent(name: "tamper", value: "clear", displayed: false)
|
result << createEvent(name: "tamper", value: "clear", displayed: false)
|
||||||
break
|
break
|
||||||
case 3:
|
case 3:
|
||||||
result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName was moved")
|
result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName was tampered")
|
||||||
break
|
break
|
||||||
case 7:
|
case 7:
|
||||||
result << motionEvent(1)
|
result << motionEvent(1)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
log.warn "Need to handle this cmd.notificationType: ${cmd.notificationType}"
|
||||||
result << createEvent(descriptionText: cmd.toString(), isStateChange: false)
|
result << createEvent(descriptionText: cmd.toString(), isStateChange: false)
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) {
|
||||||
|
log.debug "ConfigurationReport: $cmd"
|
||||||
|
def result = []
|
||||||
|
def value
|
||||||
|
if (cmd.parameterNumber == 9 && cmd.configurationValue[0] == 0) {
|
||||||
|
value = "USB Cable"
|
||||||
|
if (!isConfigured()) {
|
||||||
|
log.debug("ConfigurationReport: configuring device")
|
||||||
|
result << response(configure())
|
||||||
|
}
|
||||||
|
result << createEvent(name: "batteryStatus", value: value, displayed: false)
|
||||||
|
result << createEvent(name: "powerSupply", value: value, displayed: false)
|
||||||
|
}else if (cmd.parameterNumber == 9 && cmd.configurationValue[0] == 1) {
|
||||||
|
value = "Battery"
|
||||||
|
result << createEvent(name: "powerSupply", value: value, displayed: false)
|
||||||
|
} else if (cmd.parameterNumber == 101){
|
||||||
|
result << response(configure())
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
log.debug "General zwaveEvent cmd: ${cmd}"
|
||||||
createEvent(descriptionText: cmd.toString(), isStateChange: false)
|
createEvent(descriptionText: cmd.toString(), isStateChange: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
// This sensor joins as a secure device if you double-click the button to include it
|
// This sensor joins as a secure device if you double-click the button to include it
|
||||||
if (device.device.rawDescription =~ /98/ && !state.sec) {
|
log.debug "${device.displayName} is configuring its settings"
|
||||||
log.debug "Multi 6 not sending configure until secure"
|
def request = []
|
||||||
return []
|
|
||||||
}
|
|
||||||
log.debug "Multi 6 configure()"
|
|
||||||
def request = [
|
|
||||||
// send no-motion report 20 seconds after motion stops
|
|
||||||
zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: 20),
|
|
||||||
|
|
||||||
// report every 8 minutes (threshold reports don't work on battery power)
|
//1. set association groups for hub
|
||||||
zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 8*60),
|
request << zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)
|
||||||
|
|
||||||
// report automatically on threshold change
|
request << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId)
|
||||||
zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 1),
|
|
||||||
|
//2. automatic report flags
|
||||||
|
// param 101 -103 [4 bytes] 128: light sensor, 64 humidity, 32 temperature sensor, 2 ultraviolet sensor, 1 battery sensor -> send command 227 to get all reports
|
||||||
|
request << zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 226) //association group 1
|
||||||
|
|
||||||
|
request << zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 1) //association group 2
|
||||||
|
|
||||||
|
//3. no-motion report x seconds after motion stops (default 20 secs)
|
||||||
|
request << zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: timeOptionValueMap[motionDelayTime] ?: 20)
|
||||||
|
|
||||||
|
//4. motionSensitivity 3 levels: 64-normal (default), 127-maximum, 0-minimum
|
||||||
|
request << zwave.configurationV1.configurationSet(parameterNumber: 6, size: 1,
|
||||||
|
scaledConfigurationValue:
|
||||||
|
motionSensitivity == "normal" ? 64 :
|
||||||
|
motionSensitivity == "maximum" ? 127 :
|
||||||
|
motionSensitivity == "minimum" ? 0 : 64)
|
||||||
|
|
||||||
|
//5. report every x minutes (threshold reports don't work on battery power, default 8 mins)
|
||||||
|
request << zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: timeOptionValueMap[reportInterval] ?: 8*60) //association group 1
|
||||||
|
|
||||||
|
request << zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 6*60*60) //association group 2
|
||||||
|
|
||||||
|
//6. report automatically on threshold change
|
||||||
|
request << zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 1)
|
||||||
|
|
||||||
|
//7. query sensor data
|
||||||
|
request << zwave.batteryV1.batteryGet()
|
||||||
|
request << zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0x0C) //motion
|
||||||
|
request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01) //temperature
|
||||||
|
request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x03) //illuminance
|
||||||
|
request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x05) //humidity
|
||||||
|
request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x1B) //ultravioletIndex
|
||||||
|
|
||||||
|
setConfigured("true")
|
||||||
|
|
||||||
zwave.batteryV1.batteryGet(),
|
|
||||||
zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0x0C),
|
|
||||||
]
|
|
||||||
commands(request) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]
|
commands(request) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]
|
||||||
}
|
}
|
||||||
|
|
||||||
private setConfigured() {
|
private def getTimeOptionValueMap() { [
|
||||||
updateDataValue("configured", "true")
|
"20 seconds" : 20,
|
||||||
|
"40 seconds" : 40,
|
||||||
|
"1 minute" : 60,
|
||||||
|
"2 minutes" : 2*60,
|
||||||
|
"3 minutes" : 3*60,
|
||||||
|
"4 minutes" : 4*60,
|
||||||
|
"5 minutes" : 5*60,
|
||||||
|
"8 minutes" : 8*60,
|
||||||
|
"15 minutes" : 15*60,
|
||||||
|
"30 minutes" : 30*60,
|
||||||
|
"1 hours" : 1*60*60,
|
||||||
|
"6 hours" : 6*60*60,
|
||||||
|
"12 hours" : 12*60*60,
|
||||||
|
"18 hours" : 6*60*60,
|
||||||
|
"24 hours" : 24*60*60,
|
||||||
|
]}
|
||||||
|
|
||||||
|
private setConfigured(configure) {
|
||||||
|
updateDataValue("configured", configure)
|
||||||
}
|
}
|
||||||
|
|
||||||
private isConfigured() {
|
private isConfigured() {
|
||||||
@@ -281,5 +407,6 @@ private command(physicalgraph.zwave.Command cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private commands(commands, delay=200) {
|
private commands(commands, delay=200) {
|
||||||
|
log.info "sending commands: ${commands}"
|
||||||
delayBetween(commands.collect{ command(it) }, delay)
|
delayBetween(commands.collect{ command(it) }, delay)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,168 +38,63 @@ metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UI tile definitions
|
// UI tile definitions
|
||||||
tiles {
|
tiles(scale: 2) {
|
||||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||||
state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff"
|
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||||
state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821"
|
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
}
|
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
}
|
}
|
||||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) {
|
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||||
state "level", action:"switch level.setLevel"
|
attributeState "level", action:"switch level.setLevel"
|
||||||
}
|
}
|
||||||
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
}
|
||||||
state "level", label: 'Level ${currentValue}%'
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||||
}
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
main "switch"
|
||||||
main(["switch"])
|
details(["switch", "refresh"])
|
||||||
details(["switch", "level", "levelSliderControl", "refresh"])
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse incoming device messages to generate events
|
// Parse incoming device messages to generate events
|
||||||
|
|
||||||
def parse(String description) {
|
def parse(String description) {
|
||||||
log.trace description
|
log.debug "description is $description"
|
||||||
if (description?.startsWith("catchall:")) {
|
|
||||||
def msg = zigbee.parse(description)
|
def resultMap = zigbee.getKnownDescription(description)
|
||||||
log.trace msg
|
if (resultMap) {
|
||||||
log.trace "data: $msg.data"
|
log.info resultMap
|
||||||
|
if (resultMap.type == "update") {
|
||||||
if(description?.endsWith("0100") ||description?.endsWith("1001"))
|
log.info "$device updates: ${resultMap.value}"
|
||||||
{
|
|
||||||
def result = createEvent(name: "switch", value: "on")
|
|
||||||
log.debug "Parse returned ${result?.descriptionText}"
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
if(description?.endsWith("0000") || description?.endsWith("1000"))
|
sendEvent(name: resultMap.type, value: resultMap.value)
|
||||||
{
|
|
||||||
def result = createEvent(name: "switch", value: "off")
|
|
||||||
log.debug "Parse returned ${result?.descriptionText}"
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (description?.startsWith("read attr")) {
|
|
||||||
|
|
||||||
log.debug description[-2..-1]
|
|
||||||
def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 )
|
|
||||||
|
|
||||||
sendEvent( name: "level", value: i )
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||||
|
log.debug zigbee.parseDescriptionAsMap(description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
zigbee.off()
|
||||||
}
|
}
|
||||||
|
|
||||||
def on() {
|
def on() {
|
||||||
log.debug "on()"
|
zigbee.on()
|
||||||
sendEvent(name: "switch", value: "on")
|
|
||||||
|
|
||||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}"
|
|
||||||
}
|
|
||||||
|
|
||||||
def off() {
|
|
||||||
log.debug "off()"
|
|
||||||
sendEvent(name: "switch", value: "off")
|
|
||||||
|
|
||||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
def refresh() {
|
|
||||||
// Schedule poll every 1 min
|
|
||||||
//schedule("0 */1 * * * ?", poll)
|
|
||||||
//poll()
|
|
||||||
|
|
||||||
[
|
|
||||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
|
|
||||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def setLevel(value) {
|
def setLevel(value) {
|
||||||
log.trace "setLevel($value)"
|
zigbee.setLevel(value)
|
||||||
def cmds = []
|
}
|
||||||
|
|
||||||
if (value == 0) {
|
def refresh() {
|
||||||
sendEvent(name: "switch", value: "off")
|
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig()
|
||||||
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {0000 0000}"
|
|
||||||
}
|
|
||||||
else if (device.latestValue("switch") == "off") {
|
|
||||||
sendEvent(name: "switch", value: "on")
|
|
||||||
}
|
|
||||||
|
|
||||||
sendEvent(name: "level", value: value)
|
|
||||||
def level = hex(value * 255/100)
|
|
||||||
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}"
|
|
||||||
|
|
||||||
//log.debug cmds
|
|
||||||
cmds
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
|
log.debug "Configuring Reporting and Bindings."
|
||||||
log.debug "Configuring Reporting and Bindings."
|
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||||
def configCmds = [
|
|
||||||
|
|
||||||
//Switch Reporting
|
|
||||||
"zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500",
|
|
||||||
"send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1000",
|
|
||||||
|
|
||||||
//Level Control Reporting
|
|
||||||
"zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200",
|
|
||||||
"send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1500",
|
|
||||||
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000",
|
|
||||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 500",
|
|
||||||
]
|
|
||||||
return configCmds + refresh() // send refresh cmds as part of config
|
|
||||||
}
|
|
||||||
|
|
||||||
def uninstalled() {
|
|
||||||
|
|
||||||
log.debug "uninstalled()"
|
|
||||||
|
|
||||||
response("zcl rftd")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private getEndpointId() {
|
|
||||||
new BigInteger(device.endpointId, 16).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private hex(value, width=2) {
|
|
||||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
|
||||||
while (s.size() < width) {
|
|
||||||
s = "0" + s
|
|
||||||
}
|
|
||||||
s
|
|
||||||
}
|
|
||||||
|
|
||||||
private Integer convertHexToInt(hex) {
|
|
||||||
Integer.parseInt(hex,16)
|
|
||||||
}
|
|
||||||
|
|
||||||
private String swapEndianHex(String hex) {
|
|
||||||
reverseArray(hex.decodeHex()).encodeHex()
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] reverseArray(byte[] array) {
|
|
||||||
int i = 0;
|
|
||||||
int j = array.length - 1;
|
|
||||||
byte tmp;
|
|
||||||
while (j > i) {
|
|
||||||
tmp = array[j];
|
|
||||||
array[j] = array[i];
|
|
||||||
array[i] = tmp;
|
|
||||||
j--;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
return array
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,141 +55,136 @@ metadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
standardTile("indicator", "device.indicatorStatus", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
standardTile("indicator", "device.indicatorStatus", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
state "when off", action:"indicator.indicatorWhenOn", icon:"st.indicators.lit-when-off"
|
state "when off", action:"indicator.indicatorWhenOn", icon:"st.indicators.lit-when-off"
|
||||||
state "when on", action:"indicator.indicatorNever", icon:"st.indicators.lit-when-on"
|
state "when on", action:"indicator.indicatorNever", icon:"st.indicators.lit-when-on"
|
||||||
state "never", action:"indicator.indicatorWhenOff", icon:"st.indicators.never-lit"
|
state "never", action:"indicator.indicatorWhenOff", icon:"st.indicators.never-lit"
|
||||||
}
|
}
|
||||||
standardTile("refresh", "device.switch", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
|
||||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
standardTile("refresh", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||||
|
state "level", label:'${currentValue} %', unit:"%", backgroundColor:"#ffffff"
|
||||||
}
|
}
|
||||||
|
|
||||||
main(["switch"])
|
main(["switch"])
|
||||||
details(["switch", "refresh", "indicator"])
|
details(["switch", "level", "indicator", "refresh"])
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse(String description) {
|
def parse(String description) {
|
||||||
def item1 = [
|
def result = null
|
||||||
canBeCurrentState: false,
|
if (description != "updated") {
|
||||||
linkText: getLinkText(device),
|
log.debug "parse() >> zwave.parse($description)"
|
||||||
isStateChange: false,
|
def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 1])
|
||||||
displayed: false,
|
if (cmd) {
|
||||||
descriptionText: description,
|
result = zwaveEvent(cmd)
|
||||||
value: description
|
}
|
||||||
]
|
|
||||||
def result
|
|
||||||
def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 1])
|
|
||||||
if (cmd) {
|
|
||||||
result = createEvent(cmd, item1)
|
|
||||||
}
|
}
|
||||||
else {
|
if (result?.name == 'hail' && hubFirmwareLessThan("000.011.00602")) {
|
||||||
item1.displayed = displayed(description, item1.isStateChange)
|
result = [result, response(zwave.basicV1.basicGet())]
|
||||||
result = [item1]
|
log.debug "Was hailed: requesting state update"
|
||||||
|
} else {
|
||||||
|
log.debug "Parse returned ${result?.descriptionText}"
|
||||||
}
|
}
|
||||||
log.debug "Parse returned ${result?.descriptionText}"
|
return result
|
||||||
result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def createEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, Map item1) {
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
|
||||||
def result = doCreateEvent(cmd, item1)
|
dimmerEvents(cmd)
|
||||||
for (int i = 0; i < result.size(); i++) {
|
}
|
||||||
result[i].type = "physical"
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
|
||||||
|
dimmerEvents(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd) {
|
||||||
|
dimmerEvents(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd) {
|
||||||
|
dimmerEvents(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
private dimmerEvents(physicalgraph.zwave.Command cmd) {
|
||||||
|
def value = (cmd.value ? "on" : "off")
|
||||||
|
def result = [createEvent(name: "switch", value: value)]
|
||||||
|
if (cmd.value && cmd.value <= 100) {
|
||||||
|
result << createEvent(name: "level", value: cmd.value, unit: "%")
|
||||||
}
|
}
|
||||||
result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
def createEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd, Map item1) {
|
|
||||||
def result = doCreateEvent(cmd, item1)
|
|
||||||
for (int i = 0; i < result.size(); i++) {
|
|
||||||
result[i].type = "physical"
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStartLevelChange cmd, Map item1) {
|
|
||||||
[]
|
|
||||||
}
|
|
||||||
|
|
||||||
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd, Map item1) {
|
|
||||||
[response(zwave.basicV1.basicGet())]
|
|
||||||
}
|
|
||||||
|
|
||||||
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd, Map item1) {
|
|
||||||
def result = doCreateEvent(cmd, item1)
|
|
||||||
for (int i = 0; i < result.size(); i++) {
|
|
||||||
result[i].type = "physical"
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd, Map item1) {
|
|
||||||
def result = doCreateEvent(cmd, item1)
|
|
||||||
result[0].descriptionText = "${item1.linkText} is ${item1.value}"
|
|
||||||
result[0].handlerName = cmd.value ? "statusOn" : "statusOff"
|
|
||||||
for (int i = 0; i < result.size(); i++) {
|
|
||||||
result[i].type = "digital"
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
def doCreateEvent(physicalgraph.zwave.Command cmd, Map item1) {
|
|
||||||
def result = [item1]
|
|
||||||
|
|
||||||
item1.name = "switch"
|
|
||||||
item1.value = cmd.value ? "on" : "off"
|
|
||||||
item1.handlerName = item1.value
|
|
||||||
item1.descriptionText = "${item1.linkText} was turned ${item1.value}"
|
|
||||||
item1.canBeCurrentState = true
|
|
||||||
item1.isStateChange = isStateChange(device, item1.name, item1.value)
|
|
||||||
item1.displayed = item1.isStateChange
|
|
||||||
|
|
||||||
if (cmd.value >= 5) {
|
|
||||||
def item2 = new LinkedHashMap(item1)
|
|
||||||
item2.name = "level"
|
|
||||||
item2.value = cmd.value as String
|
|
||||||
item2.unit = "%"
|
|
||||||
item2.descriptionText = "${item1.linkText} dimmed ${item2.value} %"
|
|
||||||
item2.canBeCurrentState = true
|
|
||||||
item2.isStateChange = isStateChange(device, item2.name, item2.value)
|
|
||||||
item2.displayed = false
|
|
||||||
result << item2
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
|
def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
|
||||||
|
log.debug "ConfigurationReport $cmd"
|
||||||
def value = "when off"
|
def value = "when off"
|
||||||
if (cmd.configurationValue[0] == 1) {value = "when on"}
|
if (cmd.configurationValue[0] == 1) {value = "when on"}
|
||||||
if (cmd.configurationValue[0] == 2) {value = "never"}
|
if (cmd.configurationValue[0] == 2) {value = "never"}
|
||||||
[name: "indicatorStatus", value: value, display: false]
|
createEvent([name: "indicatorStatus", value: value])
|
||||||
}
|
}
|
||||||
|
|
||||||
def createEvent(physicalgraph.zwave.Command cmd, Map map) {
|
def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) {
|
||||||
// Handles any Z-Wave commands we aren't interested in
|
createEvent([name: "hail", value: "hail", descriptionText: "Switch button was pressed", displayed: false])
|
||||||
log.debug "UNHANDLED COMMAND $cmd"
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
|
||||||
|
log.debug "manufacturerId: ${cmd.manufacturerId}"
|
||||||
|
log.debug "manufacturerName: ${cmd.manufacturerName}"
|
||||||
|
log.debug "productId: ${cmd.productId}"
|
||||||
|
log.debug "productTypeId: ${cmd.productTypeId}"
|
||||||
|
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
|
||||||
|
updateDataValue("MSR", msr)
|
||||||
|
createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false])
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd) {
|
||||||
|
[createEvent(name:"switch", value:"on"), response(zwave.switchMultilevelV1.switchMultilevelGet().format())]
|
||||||
|
}
|
||||||
|
|
||||||
|
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||||
|
// Handles all Z-Wave commands we aren't interested in
|
||||||
|
[:]
|
||||||
}
|
}
|
||||||
|
|
||||||
def on() {
|
def on() {
|
||||||
log.info "on"
|
delayBetween([
|
||||||
delayBetween([zwave.basicV1.basicSet(value: 0xFF).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
|
zwave.basicV1.basicSet(value: 0xFF).format(),
|
||||||
|
zwave.switchMultilevelV1.switchMultilevelGet().format()
|
||||||
|
],5000)
|
||||||
}
|
}
|
||||||
|
|
||||||
def off() {
|
def off() {
|
||||||
delayBetween ([zwave.basicV1.basicSet(value: 0x00).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
|
delayBetween([
|
||||||
|
zwave.basicV1.basicSet(value: 0x00).format(),
|
||||||
|
zwave.switchMultilevelV1.switchMultilevelGet().format()
|
||||||
|
],5000)
|
||||||
}
|
}
|
||||||
|
|
||||||
def setLevel(value) {
|
def setLevel(value) {
|
||||||
|
log.debug "setLevel >> value: $value"
|
||||||
def valueaux = value as Integer
|
def valueaux = value as Integer
|
||||||
def level = Math.min(valueaux, 99)
|
def level = Math.max(Math.min(valueaux, 99), 0)
|
||||||
|
if (level > 0) {
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
} else {
|
||||||
|
sendEvent(name: "switch", value: "off")
|
||||||
|
}
|
||||||
|
sendEvent(name: "level", value: level, unit: "%")
|
||||||
delayBetween ([zwave.basicV1.basicSet(value: level).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
|
delayBetween ([zwave.basicV1.basicSet(value: level).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
|
||||||
}
|
}
|
||||||
|
|
||||||
def setLevel(value, duration) {
|
def setLevel(value, duration) {
|
||||||
|
log.debug "setLevel >> value: $value, duration: $duration"
|
||||||
def valueaux = value as Integer
|
def valueaux = value as Integer
|
||||||
def level = Math.min(valueaux, 99)
|
def level = Math.max(Math.min(valueaux, 99), 0)
|
||||||
def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60)
|
def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60)
|
||||||
zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format()
|
def getStatusDelay = duration < 128 ? (duration*1000)+2000 : (Math.round(duration / 60)*60*1000)+2000
|
||||||
|
delayBetween ([zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format(),
|
||||||
|
zwave.switchMultilevelV1.switchMultilevelGet().format()], getStatusDelay)
|
||||||
}
|
}
|
||||||
|
|
||||||
def poll() {
|
def poll() {
|
||||||
@@ -197,21 +192,27 @@ def poll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
zwave.switchMultilevelV1.switchMultilevelGet().format()
|
log.debug "refresh() is called"
|
||||||
|
def commands = []
|
||||||
|
commands << zwave.switchMultilevelV1.switchMultilevelGet().format()
|
||||||
|
if (getDataValue("MSR") == null) {
|
||||||
|
commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
|
||||||
|
}
|
||||||
|
delayBetween(commands,100)
|
||||||
}
|
}
|
||||||
|
|
||||||
def indicatorWhenOn() {
|
def indicatorWhenOn() {
|
||||||
sendEvent(name: "indicatorStatus", value: "when on", display: false)
|
sendEvent(name: "indicatorStatus", value: "when on")
|
||||||
zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 3, size: 1).format()
|
zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 3, size: 1).format()
|
||||||
}
|
}
|
||||||
|
|
||||||
def indicatorWhenOff() {
|
def indicatorWhenOff() {
|
||||||
sendEvent(name: "indicatorStatus", value: "when off", display: false)
|
sendEvent(name: "indicatorStatus", value: "when off")
|
||||||
zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 3, size: 1).format()
|
zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 3, size: 1).format()
|
||||||
}
|
}
|
||||||
|
|
||||||
def indicatorNever() {
|
def indicatorNever() {
|
||||||
sendEvent(name: "indicatorStatus", value: "never", display: false)
|
sendEvent(name: "indicatorStatus", value: "never")
|
||||||
zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 3, size: 1).format()
|
zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 3, size: 1).format()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,4 +223,4 @@ def invertSwitch(invert=true) {
|
|||||||
else {
|
else {
|
||||||
zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 4, size: 1).format()
|
zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 4, size: 1).format()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,7 @@ metadata {
|
|||||||
capability "Switch Level"
|
capability "Switch Level"
|
||||||
capability "Polling"
|
capability "Polling"
|
||||||
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019"
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019", manufacturer: "GE_Appliances", model: "ZLL Light", deviceJoinName: "GE Link Bulb"
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI tile definitions
|
// UI tile definitions
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ metadata {
|
|||||||
capability "Actuator"
|
capability "Actuator"
|
||||||
capability "Sensor"
|
capability "Sensor"
|
||||||
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45852"
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45857"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// simulator metadata
|
// simulator metadata
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ metadata {
|
|||||||
capability "Actuator"
|
capability "Actuator"
|
||||||
capability "Sensor"
|
capability "Sensor"
|
||||||
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "0003, 000A,0019", manufacturer: "Jasco Products", model: "45853"
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45856"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// simulator metadata
|
// simulator metadata
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* Logitech Harmony Activity
|
||||||
|
*
|
||||||
|
* Copyright 2015 Juan Risso
|
||||||
|
*
|
||||||
|
* 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: "Logitech Harmony Activity", namespace: "smartthings", author: "Juan Risso") {
|
||||||
|
capability "Switch"
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Refresh"
|
||||||
|
|
||||||
|
command "huboff"
|
||||||
|
command "alloff"
|
||||||
|
command "refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
// simulator metadata
|
||||||
|
simulator {
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI tile definitions
|
||||||
|
tiles {
|
||||||
|
standardTile("button", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "off", label: 'Off', action: "switch.on", icon: "st.harmony.harmony-hub-icon", backgroundColor: "#ffffff", nextState: "on"
|
||||||
|
state "on", label: 'On', action: "switch.off", icon: "st.harmony.harmony-hub-icon", backgroundColor: "#79b821", nextState: "off"
|
||||||
|
}
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
standardTile("forceoff", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'Force End', action:"switch.off", icon:"st.secondary.off"
|
||||||
|
}
|
||||||
|
standardTile("huboff", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'End Hub Action', action:"huboff", icon:"st.harmony.harmony-hub-icon"
|
||||||
|
}
|
||||||
|
standardTile("alloff", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
|
state "default", label:'All Actions', action:"alloff", icon:"st.secondary.off"
|
||||||
|
}
|
||||||
|
main "button"
|
||||||
|
details(["button", "refresh", "forceoff", "huboff", "alloff"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(String description) {
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
sendEvent(name: "switch", value: "on")
|
||||||
|
log.trace parent.activity(device.deviceNetworkId,"start")
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
sendEvent(name: "switch", value: "off")
|
||||||
|
log.trace parent.activity(device.deviceNetworkId,"end")
|
||||||
|
}
|
||||||
|
|
||||||
|
def huboff() {
|
||||||
|
sendEvent(name: "switch", value: "off")
|
||||||
|
log.trace parent.activity(device.deviceNetworkId,"hub")
|
||||||
|
}
|
||||||
|
|
||||||
|
def alloff() {
|
||||||
|
sendEvent(name: "switch", value: "off")
|
||||||
|
log.trace parent.activity("all","end")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
log.debug "Executing 'refresh'"
|
||||||
|
log.trace parent.poll()
|
||||||
|
}
|
||||||
@@ -17,44 +17,50 @@ metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
simulator {
|
simulator {
|
||||||
// TODO: define status and reply messages here
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles {
|
tiles(scale: 2) {
|
||||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||||
state "unreachable", label: "?", action:"refresh.refresh", icon:"st.switches.light.off", backgroundColor:"#666666"
|
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||||
state "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
attributeState "unreachable", label: "?", action:"refresh.refresh", icon:"http://hosted.lifx.co/smartthings/v1/196xUnreachable.png", backgroundColor:"#666666"
|
||||||
state "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
attributeState "on", label:'${name}', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
state "turningOn", label:'Turning on', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
attributeState "off", label:'${name}', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
state "turningOff", label:'Turning off', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
attributeState "turningOn", label:'Turning on', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
attributeState "turningOff", label:'Turning off', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", 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:"setColor"
|
||||||
|
}
|
||||||
|
|
||||||
|
tileAttribute ("device.model", key: "SECONDARY_CONTROL") {
|
||||||
|
attributeState "model", label: '${currentValue}'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") {
|
valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
state "default", label:''
|
state "default", label:''
|
||||||
}
|
}
|
||||||
|
|
||||||
controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) {
|
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 2, width: 4, inactiveLabel: false, range:"(2700..9000)") {
|
||||||
state "color", action:"setColor"
|
|
||||||
}
|
|
||||||
|
|
||||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") {
|
|
||||||
state "level", action:"switch level.setLevel"
|
|
||||||
}
|
|
||||||
valueTile("level", "device.level", inactiveLabel: false, icon: "st.illuminance.illuminance.light", decoration: "flat") {
|
|
||||||
state "level", label: '${currentValue}%'
|
|
||||||
}
|
|
||||||
|
|
||||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..9000)") {
|
|
||||||
state "colorTemp", action:"color temperature.setColorTemperature"
|
state "colorTemp", action:"color temperature.setColorTemperature"
|
||||||
}
|
}
|
||||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") {
|
|
||||||
|
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", height: 2, width: 2) {
|
||||||
state "colorTemp", label: '${currentValue}K'
|
state "colorTemp", label: '${currentValue}K'
|
||||||
}
|
}
|
||||||
|
|
||||||
main(["switch"])
|
main "switch"
|
||||||
details(["switch", "refresh", "level", "levelSliderControl", "rgbSelector", "colorTempSliderControl", "colorTemp"])
|
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +76,7 @@ def parse(String description) {
|
|||||||
def setHue(percentage) {
|
def setHue(percentage) {
|
||||||
log.debug "setHue ${percentage}"
|
log.debug "setHue ${percentage}"
|
||||||
parent.logErrors(logObject: log) {
|
parent.logErrors(logObject: log) {
|
||||||
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "hue:${percentage * 3.6}"])
|
def resp = parent.apiPUT("/lights/${selector()}/state", [color: "hue:${percentage * 3.6}", power: "on"])
|
||||||
if (resp.status < 300) {
|
if (resp.status < 300) {
|
||||||
sendEvent(name: "hue", value: percentage)
|
sendEvent(name: "hue", value: percentage)
|
||||||
sendEvent(name: "switch", value: "on")
|
sendEvent(name: "switch", value: "on")
|
||||||
@@ -83,7 +89,7 @@ def setHue(percentage) {
|
|||||||
def setSaturation(percentage) {
|
def setSaturation(percentage) {
|
||||||
log.debug "setSaturation ${percentage}"
|
log.debug "setSaturation ${percentage}"
|
||||||
parent.logErrors(logObject: log) {
|
parent.logErrors(logObject: log) {
|
||||||
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "saturation:${percentage / 100}"])
|
def resp = parent.apiPUT("/lights/${selector()}/state", [color: "saturation:${percentage / 100}", power: "on"])
|
||||||
if (resp.status < 300) {
|
if (resp.status < 300) {
|
||||||
sendEvent(name: "saturation", value: percentage)
|
sendEvent(name: "saturation", value: percentage)
|
||||||
sendEvent(name: "switch", value: "on")
|
sendEvent(name: "switch", value: "on")
|
||||||
@@ -114,7 +120,7 @@ def setColor(Map color) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
parent.logErrors(logObject:log) {
|
parent.logErrors(logObject:log) {
|
||||||
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: attrs.join(" ")])
|
def resp = parent.apiPUT("/lights/${selector()}/state", [color: attrs.join(" "), power: "on"])
|
||||||
if (resp.status < 300) {
|
if (resp.status < 300) {
|
||||||
sendEvent(name: "color", value: color.hex)
|
sendEvent(name: "color", value: color.hex)
|
||||||
sendEvent(name: "switch", value: "on")
|
sendEvent(name: "switch", value: "on")
|
||||||
@@ -135,9 +141,10 @@ def setLevel(percentage) {
|
|||||||
return off() // if the brightness is set to 0, just turn it off
|
return off() // if the brightness is set to 0, just turn it off
|
||||||
}
|
}
|
||||||
parent.logErrors(logObject:log) {
|
parent.logErrors(logObject:log) {
|
||||||
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", ["color": "brightness:${percentage / 100}"])
|
def resp = parent.apiPUT("/lights/${selector()}/state", ["brightness": percentage / 100, "power": "on"])
|
||||||
if (resp.status < 300) {
|
if (resp.status < 300) {
|
||||||
sendEvent(name: "level", value: percentage)
|
sendEvent(name: "level", value: percentage)
|
||||||
|
sendEvent(name: "switch.setLevel", value: percentage)
|
||||||
sendEvent(name: "switch", value: "on")
|
sendEvent(name: "switch", value: "on")
|
||||||
} else {
|
} else {
|
||||||
log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
|
log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
|
||||||
@@ -148,7 +155,7 @@ def setLevel(percentage) {
|
|||||||
def setColorTemperature(kelvin) {
|
def setColorTemperature(kelvin) {
|
||||||
log.debug "Executing 'setColorTemperature' to ${kelvin}"
|
log.debug "Executing 'setColorTemperature' to ${kelvin}"
|
||||||
parent.logErrors() {
|
parent.logErrors() {
|
||||||
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "kelvin:${kelvin}"])
|
def resp = parent.apiPUT("/lights/${selector()}/state", [color: "kelvin:${kelvin}", power: "on"])
|
||||||
if (resp.status < 300) {
|
if (resp.status < 300) {
|
||||||
sendEvent(name: "colorTemperature", value: kelvin)
|
sendEvent(name: "colorTemperature", value: kelvin)
|
||||||
sendEvent(name: "color", value: "#ffffff")
|
sendEvent(name: "color", value: "#ffffff")
|
||||||
@@ -163,7 +170,7 @@ def setColorTemperature(kelvin) {
|
|||||||
def on() {
|
def on() {
|
||||||
log.debug "Device setOn"
|
log.debug "Device setOn"
|
||||||
parent.logErrors() {
|
parent.logErrors() {
|
||||||
if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "on"]) != null) {
|
if (parent.apiPUT("/lights/${selector()}/state", [power: "on"]) != null) {
|
||||||
sendEvent(name: "switch", value: "on")
|
sendEvent(name: "switch", value: "on")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,7 +179,7 @@ def on() {
|
|||||||
def off() {
|
def off() {
|
||||||
log.debug "Device setOff"
|
log.debug "Device setOff"
|
||||||
parent.logErrors() {
|
parent.logErrors() {
|
||||||
if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "off"]) != null) {
|
if (parent.apiPUT("/lights/${selector()}/state", [power: "off"]) != null) {
|
||||||
sendEvent(name: "switch", value: "off")
|
sendEvent(name: "switch", value: "off")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,19 +187,26 @@ def off() {
|
|||||||
|
|
||||||
def poll() {
|
def poll() {
|
||||||
log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}"
|
log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}"
|
||||||
def resp = parent.apiGET("/lights/${device.deviceNetworkId}")
|
def resp = parent.apiGET("/lights/${selector()}")
|
||||||
if (resp.status != 200) {
|
if (resp.status == 404) {
|
||||||
|
sendEvent(name: "switch", value: "unreachable")
|
||||||
|
return []
|
||||||
|
} else if (resp.status != 200) {
|
||||||
log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}")
|
log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}")
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
def data = resp.data
|
def data = resp.data[0]
|
||||||
|
log.debug("Data: ${data}")
|
||||||
|
|
||||||
sendEvent(name: "level", value: sprintf("%.1f", (data.brightness ?: 1) * 100))
|
sendEvent(name: "label", value: data.label)
|
||||||
|
sendEvent(name: "level", value: Math.round((data.brightness ?: 1) * 100))
|
||||||
|
sendEvent(name: "switch.setLevel", value: Math.round((data.brightness ?: 1) * 100))
|
||||||
sendEvent(name: "switch", value: data.connected ? data.power : "unreachable")
|
sendEvent(name: "switch", value: data.connected ? data.power : "unreachable")
|
||||||
sendEvent(name: "color", value: colorUtil.hslToHex((data.color.hue / 3.6) as int, (data.color.saturation * 100) as int))
|
sendEvent(name: "color", value: colorUtil.hslToHex((data.color.hue / 3.6) as int, (data.color.saturation * 100) as int))
|
||||||
sendEvent(name: "hue", value: data.color.hue / 3.6)
|
sendEvent(name: "hue", value: data.color.hue / 3.6)
|
||||||
sendEvent(name: "saturation", value: data.color.saturation * 100)
|
sendEvent(name: "saturation", value: data.color.saturation * 100)
|
||||||
sendEvent(name: "colorTemperature", value: data.color.kelvin)
|
sendEvent(name: "colorTemperature", value: data.color.kelvin)
|
||||||
|
sendEvent(name: "model", value: "${data.product.company} ${data.product.name}")
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -201,3 +215,11 @@ def refresh() {
|
|||||||
log.debug "Executing 'refresh'"
|
log.debug "Executing 'refresh'"
|
||||||
poll()
|
poll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def selector() {
|
||||||
|
if (device.deviceNetworkId.contains(":")) {
|
||||||
|
return device.deviceNetworkId
|
||||||
|
} else {
|
||||||
|
return "id:${device.deviceNetworkId}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,41 +16,44 @@ metadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
simulator {
|
simulator {
|
||||||
// TODO: define status and reply messages here
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles {
|
tiles(scale: 2) {
|
||||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||||
state "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||||
state "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
attributeState "unreachable", label: "?", action:"refresh.refresh", icon:"http://hosted.lifx.co/smartthings/v1/196xUnreachable.png", backgroundColor:"#666666"
|
||||||
state "turningOn", label:'Turning on', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
attributeState "on", label:'${name}', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
state "turningOff", label:'Turning off', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
attributeState "off", label:'${name}', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
state "unreachable", label: "?", action:"refresh.refresh", icon:"st.switches.light.off", backgroundColor:"#666666"
|
attributeState "turningOn", label:'Turning on', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
attributeState "turningOff", label:'Turning off', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
|
||||||
|
}
|
||||||
|
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||||
|
attributeState "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
}
|
}
|
||||||
|
|
||||||
valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") {
|
valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||||
state "default", label:''
|
state "default", label:''
|
||||||
}
|
}
|
||||||
|
|
||||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") {
|
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 2, width: 4, inactiveLabel: false, range:"(2700..9000)") {
|
||||||
state "level", action:"switch level.setLevel"
|
|
||||||
}
|
|
||||||
valueTile("level", "device.level", inactiveLabel: false, icon: "st.illuminance.illuminance.light", decoration: "flat") {
|
|
||||||
state "level", label: '${currentValue}%'
|
|
||||||
}
|
|
||||||
|
|
||||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") {
|
|
||||||
state "colorTemp", action:"color temperature.setColorTemperature"
|
state "colorTemp", action:"color temperature.setColorTemperature"
|
||||||
}
|
}
|
||||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") {
|
|
||||||
|
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", height: 2, width: 2) {
|
||||||
state "colorTemp", label: '${currentValue}K'
|
state "colorTemp", label: '${currentValue}K'
|
||||||
}
|
}
|
||||||
|
|
||||||
main(["switch"])
|
main "switch"
|
||||||
details(["switch", "refresh", "level", "levelSliderControl", "colorTempSliderControl", "colorTemp"])
|
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse events into attributes
|
// parse events into attributes
|
||||||
@@ -72,9 +75,10 @@ def setLevel(percentage) {
|
|||||||
return off() // if the brightness is set to 0, just turn it off
|
return off() // if the brightness is set to 0, just turn it off
|
||||||
}
|
}
|
||||||
parent.logErrors(logObject:log) {
|
parent.logErrors(logObject:log) {
|
||||||
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", ["color": "brightness:${percentage / 100}"])
|
def resp = parent.apiPUT("/lights/${selector()}/state", [brightness: percentage / 100, power: "on"])
|
||||||
if (resp.status < 300) {
|
if (resp.status < 300) {
|
||||||
sendEvent(name: "level", value: percentage)
|
sendEvent(name: "level", value: percentage)
|
||||||
|
sendEvent(name: "switch.setLevel", value: percentage)
|
||||||
sendEvent(name: "switch", value: "on")
|
sendEvent(name: "switch", value: "on")
|
||||||
} else {
|
} else {
|
||||||
log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
|
log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
|
||||||
@@ -85,7 +89,7 @@ def setLevel(percentage) {
|
|||||||
def setColorTemperature(kelvin) {
|
def setColorTemperature(kelvin) {
|
||||||
log.debug "Executing 'setColorTemperature' to ${kelvin}"
|
log.debug "Executing 'setColorTemperature' to ${kelvin}"
|
||||||
parent.logErrors() {
|
parent.logErrors() {
|
||||||
def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "kelvin:${kelvin}"])
|
def resp = parent.apiPUT("/lights/${selector()}/state", [color: "kelvin:${kelvin}", power: "on"])
|
||||||
if (resp.status < 300) {
|
if (resp.status < 300) {
|
||||||
sendEvent(name: "colorTemperature", value: kelvin)
|
sendEvent(name: "colorTemperature", value: kelvin)
|
||||||
sendEvent(name: "color", value: "#ffffff")
|
sendEvent(name: "color", value: "#ffffff")
|
||||||
@@ -100,7 +104,7 @@ def setColorTemperature(kelvin) {
|
|||||||
def on() {
|
def on() {
|
||||||
log.debug "Device setOn"
|
log.debug "Device setOn"
|
||||||
parent.logErrors() {
|
parent.logErrors() {
|
||||||
if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "on"]) != null) {
|
if (parent.apiPUT("/lights/${selector()}/state", [power: "on"]) != null) {
|
||||||
sendEvent(name: "switch", value: "on")
|
sendEvent(name: "switch", value: "on")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,7 +113,7 @@ def on() {
|
|||||||
def off() {
|
def off() {
|
||||||
log.debug "Device setOff"
|
log.debug "Device setOff"
|
||||||
parent.logErrors() {
|
parent.logErrors() {
|
||||||
if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "off"]) != null) {
|
if (parent.apiPUT("/lights/${selector()}/state", [power: "off"]) != null) {
|
||||||
sendEvent(name: "switch", value: "off")
|
sendEvent(name: "switch", value: "off")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,16 +121,22 @@ def off() {
|
|||||||
|
|
||||||
def poll() {
|
def poll() {
|
||||||
log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}"
|
log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}"
|
||||||
def resp = parent.apiGET("/lights/${device.deviceNetworkId}")
|
def resp = parent.apiGET("/lights/${selector()}")
|
||||||
if (resp.status != 200) {
|
if (resp.status == 404) {
|
||||||
|
sendEvent(name: "switch", value: "unreachable")
|
||||||
|
return []
|
||||||
|
} else if (resp.status != 200) {
|
||||||
log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}")
|
log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}")
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
def data = resp.data
|
def data = resp.data[0]
|
||||||
|
|
||||||
sendEvent(name: "level", value: sprintf("%f", (data.brightness ?: 1) * 100))
|
sendEvent(name: "label", value: data.label)
|
||||||
|
sendEvent(name: "level", value: Math.round((data.brightness ?: 1) * 100))
|
||||||
|
sendEvent(name: "switch.setLevel", value: Math.round((data.brightness ?: 1) * 100))
|
||||||
sendEvent(name: "switch", value: data.connected ? data.power : "unreachable")
|
sendEvent(name: "switch", value: data.connected ? data.power : "unreachable")
|
||||||
sendEvent(name: "colorTemperature", value: data.color.kelvin)
|
sendEvent(name: "colorTemperature", value: data.color.kelvin)
|
||||||
|
sendEvent(name: "model", value: data.product.name)
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -135,3 +145,11 @@ def refresh() {
|
|||||||
log.debug "Executing 'refresh'"
|
log.debug "Executing 'refresh'"
|
||||||
poll()
|
poll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def selector() {
|
||||||
|
if (device.deviceNetworkId.contains(":")) {
|
||||||
|
return device.deviceNetworkId
|
||||||
|
} else {
|
||||||
|
return "id:${device.deviceNetworkId}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
that issue by using state variables
|
that issue by using state variables
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
|
||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "OSRAM LIGHTIFY LED Tunable White 60W", namespace: "smartthings", author: "SmartThings") {
|
definition (name: "OSRAM LIGHTIFY LED Tunable White 60W", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
|
||||||
@@ -20,10 +22,7 @@ metadata {
|
|||||||
|
|
||||||
// indicates that device keeps track of heartbeat (in state.heartbeat)
|
// indicates that device keeps track of heartbeat (in state.heartbeat)
|
||||||
attribute "heartbeat", "string"
|
attribute "heartbeat", "string"
|
||||||
|
|
||||||
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 TW"
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 Tunable White"
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
* for the specific language governing permissions and limitations under the License.
|
* for the specific language governing permissions and limitations under the License.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
|
||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "Sylvania Ultra iQ", namespace:"smartthings", author: "SmartThings") {
|
definition (name: "Sylvania Ultra iQ", namespace:"smartthings", author: "SmartThings") {
|
||||||
capability "Switch Level"
|
capability "Switch Level"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
metadata {
|
metadata {
|
||||||
definition (name: "Color Control Capability", namespace: "capabilities", author: "SmartThings") {
|
definition (name: "Simulated Color Control", namespace: "smartthings/testing", author: "SmartThings") {
|
||||||
capability "Color Control"
|
capability "Color Control"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
* Thanks to Chad Monroe @cmonroe and Patrick Stuart @pstuart
|
* Thanks to Chad Monroe @cmonroe and Patrick Stuart @pstuart
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
|
||||||
|
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "WeMo Bulb", namespace: "smartthings", author: "SmartThings") {
|
definition (name: "WeMo Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
|
||||||
@@ -25,7 +27,6 @@ metadata {
|
|||||||
capability "Switch"
|
capability "Switch"
|
||||||
capability "Switch Level"
|
capability "Switch Level"
|
||||||
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,FF00", outClusters: "0019"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// simulator metadata
|
// simulator metadata
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ metadata {
|
|||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
capability "Sensor"
|
capability "Sensor"
|
||||||
|
|
||||||
|
attribute "currentIP", "string"
|
||||||
|
|
||||||
command "subscribe"
|
command "subscribe"
|
||||||
command "resubscribe"
|
command "resubscribe"
|
||||||
command "unsubscribe"
|
command "unsubscribe"
|
||||||
@@ -34,21 +36,36 @@ metadata {
|
|||||||
// simulator metadata
|
// simulator metadata
|
||||||
simulator {}
|
simulator {}
|
||||||
|
|
||||||
// UI tile definitions
|
// UI tile definitions
|
||||||
tiles {
|
tiles(scale: 2) {
|
||||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
multiAttributeTile(name:"rich-control", type: "switch", canChangeIcon: true){
|
||||||
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||||
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
attributeState "on", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
state "turningOn", label:'${name}', icon:"st.switches.switch.on", backgroundColor:"#79b821"
|
attributeState "off", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
state "turningOff", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ffffff"
|
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
}
|
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
attributeState "offline", label:'${name}', icon:"st.Home.home30", backgroundColor:"#ff0000"
|
||||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
}
|
||||||
}
|
tileAttribute ("currentIP", key: "SECONDARY_CONTROL") {
|
||||||
|
attributeState "currentIP", label: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
main "switch"
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
details (["switch", "refresh"])
|
state "on", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
}
|
state "off", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
state "turningOn", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "turningOff", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
state "offline", label:'${name}', icon:"st.Home.home30", backgroundColor:"#ff0000"
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["switch"])
|
||||||
|
details(["rich-control", "refresh"])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse events into attributes
|
// parse events into attributes
|
||||||
@@ -68,6 +85,7 @@ def parse(String description) {
|
|||||||
def result = []
|
def result = []
|
||||||
def bodyString = msg.body
|
def bodyString = msg.body
|
||||||
if (bodyString) {
|
if (bodyString) {
|
||||||
|
unschedule("setOffline")
|
||||||
def body = new XmlSlurper().parseText(bodyString)
|
def body = new XmlSlurper().parseText(bodyString)
|
||||||
|
|
||||||
if (body?.property?.TimeSyncRequest?.text()) {
|
if (body?.property?.TimeSyncRequest?.text()) {
|
||||||
@@ -78,13 +96,14 @@ def parse(String description) {
|
|||||||
} else if (body?.property?.BinaryState?.text()) {
|
} else if (body?.property?.BinaryState?.text()) {
|
||||||
def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
|
def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
|
||||||
log.trace "Notify: BinaryState = ${value}"
|
log.trace "Notify: BinaryState = ${value}"
|
||||||
result << createEvent(name: "switch", value: value)
|
result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}")
|
||||||
} else if (body?.property?.TimeZoneNotification?.text()) {
|
} else if (body?.property?.TimeZoneNotification?.text()) {
|
||||||
log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}"
|
log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}"
|
||||||
} else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) {
|
} else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) {
|
||||||
def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
|
def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
|
||||||
log.trace "GetBinaryResponse: BinaryState = ${value}"
|
log.trace "GetBinaryResponse: BinaryState = ${value}"
|
||||||
result << createEvent(name: "switch", value: value)
|
def dispaux = device.currentValue("switch") != value
|
||||||
|
result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,14 +120,6 @@ private getCallBackAddress() {
|
|||||||
device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP")
|
device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP")
|
||||||
}
|
}
|
||||||
|
|
||||||
private Integer convertHexToInt(hex) {
|
|
||||||
Integer.parseInt(hex,16)
|
|
||||||
}
|
|
||||||
|
|
||||||
private String convertHexToIP(hex) {
|
|
||||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
|
||||||
}
|
|
||||||
|
|
||||||
private getHostAddress() {
|
private getHostAddress() {
|
||||||
def ip = getDataValue("ip")
|
def ip = getDataValue("ip")
|
||||||
def port = getDataValue("port")
|
def port = getDataValue("port")
|
||||||
@@ -195,6 +206,8 @@ def subscribe(ip, port) {
|
|||||||
if (ip && ip != existingIp) {
|
if (ip && ip != existingIp) {
|
||||||
log.debug "Updating ip from $existingIp to $ip"
|
log.debug "Updating ip from $existingIp to $ip"
|
||||||
updateDataValue("ip", ip)
|
updateDataValue("ip", ip)
|
||||||
|
def ipvalue = convertHexToIP(getDataValue("ip"))
|
||||||
|
sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}")
|
||||||
}
|
}
|
||||||
if (port && port != existingPort) {
|
if (port && port != existingPort) {
|
||||||
log.debug "Updating port from $existingPort to $port"
|
log.debug "Updating port from $existingPort to $port"
|
||||||
@@ -259,6 +272,8 @@ User-Agent: CyberGarage-HTTP/1.0
|
|||||||
|
|
||||||
def poll() {
|
def poll() {
|
||||||
log.debug "Executing 'poll'"
|
log.debug "Executing 'poll'"
|
||||||
|
if (device.currentValue("currentIP") != "Offline")
|
||||||
|
runIn(10, setOffline)
|
||||||
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||||
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
||||||
Content-Length: 277
|
Content-Length: 277
|
||||||
@@ -274,3 +289,15 @@ User-Agent: CyberGarage-HTTP/1.0
|
|||||||
</s:Body>
|
</s:Body>
|
||||||
</s:Envelope>""", physicalgraph.device.Protocol.LAN)
|
</s:Envelope>""", physicalgraph.device.Protocol.LAN)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def setOffline() {
|
||||||
|
sendEvent(name: "switch", value: "offline", descriptionText: "The device is offline")
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer convertHexToInt(hex) {
|
||||||
|
Integer.parseInt(hex,16)
|
||||||
|
}
|
||||||
|
|
||||||
|
private String convertHexToIP(hex) {
|
||||||
|
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
capability "Sensor"
|
capability "Sensor"
|
||||||
|
|
||||||
|
attribute "currentIP", "string"
|
||||||
|
|
||||||
command "subscribe"
|
command "subscribe"
|
||||||
command "resubscribe"
|
command "resubscribe"
|
||||||
command "unsubscribe"
|
command "unsubscribe"
|
||||||
@@ -31,17 +33,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UI tile definitions
|
// UI tile definitions
|
||||||
tiles {
|
tiles(scale: 2) {
|
||||||
|
multiAttributeTile(name:"rich-control", type: "motion", canChangeIcon: true){
|
||||||
|
tileAttribute ("device.motion", key: "PRIMARY_CONTROL") {
|
||||||
|
attributeState "active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0"
|
||||||
|
attributeState "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff"
|
||||||
|
attributeState "offline", label:'${name}', icon:"st.motion.motion.active", backgroundColor:"#ff0000"
|
||||||
|
}
|
||||||
|
tileAttribute ("currentIP", key: "SECONDARY_CONTROL") {
|
||||||
|
attributeState "currentIP", label: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
standardTile("motion", "device.motion", width: 2, height: 2) {
|
standardTile("motion", "device.motion", width: 2, height: 2) {
|
||||||
state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0")
|
state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0")
|
||||||
state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff")
|
state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff")
|
||||||
}
|
state("offline", label:'${name}', icon:"st.motion.motion.inactive", backgroundColor:"#ff0000")
|
||||||
standardTile("refresh", "device.motion", inactiveLabel: false, decoration: "flat") {
|
|
||||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
main "motion"
|
main "motion"
|
||||||
details (["motion", "refresh"])
|
details (["rich-control", "refresh"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +77,7 @@ def parse(String description) {
|
|||||||
def result = []
|
def result = []
|
||||||
def bodyString = msg.body
|
def bodyString = msg.body
|
||||||
if (bodyString) {
|
if (bodyString) {
|
||||||
|
unschedule("setOffline")
|
||||||
def body = new XmlSlurper().parseText(bodyString)
|
def body = new XmlSlurper().parseText(bodyString)
|
||||||
|
|
||||||
if (body?.property?.TimeSyncRequest?.text()) {
|
if (body?.property?.TimeSyncRequest?.text()) {
|
||||||
@@ -72,7 +88,7 @@ def parse(String description) {
|
|||||||
} else if (body?.property?.BinaryState?.text()) {
|
} else if (body?.property?.BinaryState?.text()) {
|
||||||
def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "active" : "inactive"
|
def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "active" : "inactive"
|
||||||
log.debug "Notify - BinaryState = ${value}"
|
log.debug "Notify - BinaryState = ${value}"
|
||||||
result << createEvent(name: "motion", value: value)
|
result << createEvent(name: "motion", value: value, descriptionText: "Motion is ${value}")
|
||||||
} else if (body?.property?.TimeZoneNotification?.text()) {
|
} else if (body?.property?.TimeZoneNotification?.text()) {
|
||||||
log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}"
|
log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}"
|
||||||
}
|
}
|
||||||
@@ -91,14 +107,6 @@ private getCallBackAddress() {
|
|||||||
device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP")
|
device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP")
|
||||||
}
|
}
|
||||||
|
|
||||||
private Integer convertHexToInt(hex) {
|
|
||||||
Integer.parseInt(hex,16)
|
|
||||||
}
|
|
||||||
|
|
||||||
private String convertHexToIP(hex) {
|
|
||||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
|
||||||
}
|
|
||||||
|
|
||||||
private getHostAddress() {
|
private getHostAddress() {
|
||||||
def ip = getDataValue("ip")
|
def ip = getDataValue("ip")
|
||||||
def port = getDataValue("port")
|
def port = getDataValue("port")
|
||||||
@@ -125,6 +133,8 @@ def refresh() {
|
|||||||
////////////////////////////
|
////////////////////////////
|
||||||
def getStatus() {
|
def getStatus() {
|
||||||
log.debug "Executing WeMo Motion 'getStatus'"
|
log.debug "Executing WeMo Motion 'getStatus'"
|
||||||
|
if (device.currentValue("currentIP") != "Offline")
|
||||||
|
runIn(10, setOffline)
|
||||||
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||||
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
||||||
Content-Length: 277
|
Content-Length: 277
|
||||||
@@ -165,7 +175,9 @@ def subscribe(ip, port) {
|
|||||||
def existingPort = getDataValue("port")
|
def existingPort = getDataValue("port")
|
||||||
if (ip && ip != existingIp) {
|
if (ip && ip != existingIp) {
|
||||||
log.debug "Updating ip from $existingIp to $ip"
|
log.debug "Updating ip from $existingIp to $ip"
|
||||||
updateDataValue("ip", ip)
|
updateDataValue("ip", ip)
|
||||||
|
def ipvalue = convertHexToIP(getDataValue("ip"))
|
||||||
|
sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}")
|
||||||
}
|
}
|
||||||
if (port && port != existingPort) {
|
if (port && port != existingPort) {
|
||||||
log.debug "Updating port from $existingPort to $port"
|
log.debug "Updating port from $existingPort to $port"
|
||||||
@@ -226,3 +238,15 @@ User-Agent: CyberGarage-HTTP/1.0
|
|||||||
</s:Envelope>
|
</s:Envelope>
|
||||||
""", physicalgraph.device.Protocol.LAN)
|
""", physicalgraph.device.Protocol.LAN)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def setOffline() {
|
||||||
|
sendEvent(name: "motion", value: "offline", descriptionText: "The device is offline")
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer convertHexToInt(hex) {
|
||||||
|
Integer.parseInt(hex,16)
|
||||||
|
}
|
||||||
|
|
||||||
|
private String convertHexToIP(hex) {
|
||||||
|
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,120 +10,142 @@
|
|||||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
* 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.
|
* for the specific language governing permissions and limitations under the License.
|
||||||
*
|
*
|
||||||
* Wemo Switch
|
* Wemo Switch
|
||||||
*
|
*
|
||||||
* Author: superuser
|
* Author: Juan Risso (SmartThings)
|
||||||
* Date: 2013-10-11
|
* Date: 2015-10-11
|
||||||
*/
|
*/
|
||||||
metadata {
|
metadata {
|
||||||
definition (name: "Wemo Switch", namespace: "smartthings", author: "SmartThings") {
|
definition (name: "Wemo Switch", namespace: "smartthings", author: "SmartThings") {
|
||||||
capability "Actuator"
|
capability "Actuator"
|
||||||
capability "Switch"
|
capability "Switch"
|
||||||
capability "Polling"
|
capability "Polling"
|
||||||
capability "Refresh"
|
capability "Refresh"
|
||||||
capability "Sensor"
|
capability "Sensor"
|
||||||
|
|
||||||
command "subscribe"
|
attribute "currentIP", "string"
|
||||||
command "resubscribe"
|
|
||||||
command "unsubscribe"
|
|
||||||
}
|
|
||||||
|
|
||||||
// simulator metadata
|
command "subscribe"
|
||||||
simulator {}
|
command "resubscribe"
|
||||||
|
command "unsubscribe"
|
||||||
|
}
|
||||||
|
|
||||||
// UI tile definitions
|
// simulator metadata
|
||||||
tiles {
|
simulator {}
|
||||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
|
||||||
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821"
|
|
||||||
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff"
|
|
||||||
}
|
|
||||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
|
||||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
|
||||||
}
|
|
||||||
|
|
||||||
main "switch"
|
// UI tile definitions
|
||||||
details (["switch", "refresh"])
|
tiles(scale: 2) {
|
||||||
}
|
multiAttributeTile(name:"rich-control", type: "switch", canChangeIcon: true){
|
||||||
|
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||||
|
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
attributeState "offline", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ff0000"
|
||||||
|
}
|
||||||
|
tileAttribute ("currentIP", key: "SECONDARY_CONTROL") {
|
||||||
|
attributeState "currentIP", label: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||||
|
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
state "offline", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ff0000"
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["switch"])
|
||||||
|
details(["rich-control", "refresh"])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse events into attributes
|
// parse events into attributes
|
||||||
def parse(String description) {
|
def parse(String description) {
|
||||||
log.debug "Parsing '${description}'"
|
log.debug "Parsing '${description}'"
|
||||||
|
|
||||||
def msg = parseLanMessage(description)
|
def msg = parseLanMessage(description)
|
||||||
def headerString = msg.header
|
def headerString = msg.header
|
||||||
|
|
||||||
if (headerString?.contains("SID: uuid:")) {
|
if (headerString?.contains("SID: uuid:")) {
|
||||||
def sid = (headerString =~ /SID: uuid:.*/) ? ( headerString =~ /SID: uuid:.*/)[0] : "0"
|
def sid = (headerString =~ /SID: uuid:.*/) ? ( headerString =~ /SID: uuid:.*/)[0] : "0"
|
||||||
sid -= "SID: uuid:".trim()
|
sid -= "SID: uuid:".trim()
|
||||||
|
|
||||||
updateDataValue("subscriptionId", sid)
|
updateDataValue("subscriptionId", sid)
|
||||||
}
|
}
|
||||||
|
|
||||||
def result = []
|
def result = []
|
||||||
def bodyString = msg.body
|
def bodyString = msg.body
|
||||||
if (bodyString) {
|
if (bodyString) {
|
||||||
def body = new XmlSlurper().parseText(bodyString)
|
unschedule("setOffline")
|
||||||
|
def body = new XmlSlurper().parseText(bodyString)
|
||||||
if (body?.property?.TimeSyncRequest?.text()) {
|
if (body?.property?.TimeSyncRequest?.text()) {
|
||||||
log.trace "Got TimeSyncRequest"
|
log.trace "Got TimeSyncRequest"
|
||||||
result << timeSyncResponse()
|
result << timeSyncResponse()
|
||||||
} else if (body?.Body?.SetBinaryStateResponse?.BinaryState?.text()) {
|
} else if (body?.Body?.SetBinaryStateResponse?.BinaryState?.text()) {
|
||||||
log.trace "Got SetBinaryStateResponse = ${body?.Body?.SetBinaryStateResponse?.BinaryState?.text()}"
|
log.trace "Got SetBinaryStateResponse = ${body?.Body?.SetBinaryStateResponse?.BinaryState?.text()}"
|
||||||
} else if (body?.property?.BinaryState?.text()) {
|
} else if (body?.property?.BinaryState?.text()) {
|
||||||
def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
|
def value = body?.property?.BinaryState?.text().substring(0, 1).toInteger() == 0 ? "off" : "on"
|
||||||
log.trace "Notify: BinaryState = ${value}"
|
log.trace "Notify: BinaryState = ${value}, ${body.property.BinaryState}"
|
||||||
result << createEvent(name: "switch", value: value)
|
def dispaux = device.currentValue("switch") != value
|
||||||
} else if (body?.property?.TimeZoneNotification?.text()) {
|
result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux)
|
||||||
log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}"
|
} else if (body?.property?.TimeZoneNotification?.text()) {
|
||||||
} else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) {
|
log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}"
|
||||||
def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
|
} else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) {
|
||||||
log.trace "GetBinaryResponse: BinaryState = ${value}"
|
def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().substring(0, 1).toInteger() == 0 ? "off" : "on"
|
||||||
result << createEvent(name: "switch", value: value)
|
log.trace "GetBinaryResponse: BinaryState = ${value}, ${body.property.BinaryState}"
|
||||||
}
|
log.info "Connection: ${device.currentValue("connection")}"
|
||||||
}
|
if (device.currentValue("currentIP") == "Offline") {
|
||||||
|
def ipvalue = convertHexToIP(getDataValue("ip"))
|
||||||
result
|
sendEvent(name: "IP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
|
||||||
|
}
|
||||||
|
def dispaux2 = device.currentValue("switch") != value
|
||||||
|
result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTime() {
|
private getTime() {
|
||||||
// This is essentially System.currentTimeMillis()/1000, but System is disallowed by the sandbox.
|
// This is essentially System.currentTimeMillis()/1000, but System is disallowed by the sandbox.
|
||||||
((new GregorianCalendar().time.time / 1000l).toInteger()).toString()
|
((new GregorianCalendar().time.time / 1000l).toInteger()).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCallBackAddress() {
|
private getCallBackAddress() {
|
||||||
device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP")
|
device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP")
|
||||||
}
|
}
|
||||||
|
|
||||||
private Integer convertHexToInt(hex) {
|
private Integer convertHexToInt(hex) {
|
||||||
Integer.parseInt(hex,16)
|
Integer.parseInt(hex,16)
|
||||||
}
|
}
|
||||||
|
|
||||||
private String convertHexToIP(hex) {
|
private String convertHexToIP(hex) {
|
||||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||||
}
|
}
|
||||||
|
|
||||||
private getHostAddress() {
|
private getHostAddress() {
|
||||||
def ip = getDataValue("ip")
|
def ip = getDataValue("ip")
|
||||||
def port = getDataValue("port")
|
def port = getDataValue("port")
|
||||||
|
if (!ip || !port) {
|
||||||
if (!ip || !port) {
|
def parts = device.deviceNetworkId.split(":")
|
||||||
def parts = device.deviceNetworkId.split(":")
|
if (parts.length == 2) {
|
||||||
if (parts.length == 2) {
|
ip = parts[0]
|
||||||
ip = parts[0]
|
port = parts[1]
|
||||||
port = parts[1]
|
} else {
|
||||||
} else {
|
log.warn "Can't figure out ip and port for device: ${device.id}"
|
||||||
log.warn "Can't figure out ip and port for device: ${device.id}"
|
}
|
||||||
}
|
}
|
||||||
}
|
log.debug "Using ip: ${ip} and port: ${port} for device: ${device.id}"
|
||||||
log.debug "Using ip: ${ip} and port: ${port} for device: ${device.id}"
|
return convertHexToIP(ip) + ":" + convertHexToInt(port)
|
||||||
return convertHexToIP(ip) + ":" + convertHexToInt(port)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def on() {
|
def on() {
|
||||||
log.debug "Executing 'on'"
|
log.debug "Executing 'on'"
|
||||||
sendEvent(name: "switch", value: "on")
|
|
||||||
def turnOn = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
def turnOn = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||||
SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState"
|
SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState"
|
||||||
Host: ${getHostAddress()}
|
Host: ${getHostAddress()}
|
||||||
@@ -133,17 +155,16 @@ Content-Length: 333
|
|||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
||||||
<SOAP-ENV:Body>
|
<SOAP-ENV:Body>
|
||||||
<m:SetBinaryState xmlns:m="urn:Belkin:service:basicevent:1">
|
<m:SetBinaryState xmlns:m="urn:Belkin:service:basicevent:1">
|
||||||
<BinaryState>1</BinaryState>
|
<BinaryState>1</BinaryState>
|
||||||
</m:SetBinaryState>
|
</m:SetBinaryState>
|
||||||
</SOAP-ENV:Body>
|
</SOAP-ENV:Body>
|
||||||
</SOAP-ENV:Envelope>""", physicalgraph.device.Protocol.LAN)
|
</SOAP-ENV:Envelope>""", physicalgraph.device.Protocol.LAN)
|
||||||
}
|
}
|
||||||
|
|
||||||
def off() {
|
def off() {
|
||||||
log.debug "Executing 'off'"
|
log.debug "Executing 'off'"
|
||||||
sendEvent(name: "switch", value: "off")
|
def turnOff = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||||
def turnOff = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
|
||||||
SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState"
|
SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState"
|
||||||
Host: ${getHostAddress()}
|
Host: ${getHostAddress()}
|
||||||
Content-Type: text/xml
|
Content-Type: text/xml
|
||||||
@@ -152,36 +173,13 @@ Content-Length: 333
|
|||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
||||||
<SOAP-ENV:Body>
|
<SOAP-ENV:Body>
|
||||||
<m:SetBinaryState xmlns:m="urn:Belkin:service:basicevent:1">
|
<m:SetBinaryState xmlns:m="urn:Belkin:service:basicevent:1">
|
||||||
<BinaryState>0</BinaryState>
|
<BinaryState>0</BinaryState>
|
||||||
</m:SetBinaryState>
|
</m:SetBinaryState>
|
||||||
</SOAP-ENV:Body>
|
</SOAP-ENV:Body>
|
||||||
</SOAP-ENV:Envelope>""", physicalgraph.device.Protocol.LAN)
|
</SOAP-ENV:Envelope>""", physicalgraph.device.Protocol.LAN)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*def refresh() {
|
|
||||||
log.debug "Executing 'refresh'"
|
|
||||||
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
|
||||||
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
|
||||||
Content-Length: 277
|
|
||||||
Content-Type: text/xml; charset="utf-8"
|
|
||||||
HOST: ${getHostAddress()}
|
|
||||||
User-Agent: CyberGarage-HTTP/1.0
|
|
||||||
|
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
|
||||||
<s:Body>
|
|
||||||
<u:GetBinaryState xmlns:u="urn:Belkin:service:basicevent:1">
|
|
||||||
</u:GetBinaryState>
|
|
||||||
</s:Body>
|
|
||||||
</s:Envelope>""", physicalgraph.device.Protocol.LAN)
|
|
||||||
}*/
|
|
||||||
|
|
||||||
def refresh() {
|
|
||||||
log.debug "Executing WeMo Switch 'subscribe', then 'timeSyncResponse', then 'poll'"
|
|
||||||
[subscribe(), timeSyncResponse(), poll()]
|
|
||||||
}
|
|
||||||
|
|
||||||
def subscribe(hostAddress) {
|
def subscribe(hostAddress) {
|
||||||
log.debug "Executing 'subscribe()'"
|
log.debug "Executing 'subscribe()'"
|
||||||
def address = getCallBackAddress()
|
def address = getCallBackAddress()
|
||||||
@@ -200,27 +198,30 @@ def subscribe() {
|
|||||||
subscribe(getHostAddress())
|
subscribe(getHostAddress())
|
||||||
}
|
}
|
||||||
|
|
||||||
def subscribe(ip, port) {
|
def refresh() {
|
||||||
def existingIp = getDataValue("ip")
|
log.debug "Executing WeMo Switch 'subscribe', then 'timeSyncResponse', then 'poll'"
|
||||||
def existingPort = getDataValue("port")
|
[subscribe(), timeSyncResponse(), poll()]
|
||||||
if (ip && ip != existingIp) {
|
}
|
||||||
log.debug "Updating ip from $existingIp to $ip"
|
|
||||||
updateDataValue("ip", ip)
|
|
||||||
}
|
|
||||||
if (port && port != existingPort) {
|
|
||||||
log.debug "Updating port from $existingPort to $port"
|
|
||||||
updateDataValue("port", port)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
def subscribe(ip, port) {
|
||||||
|
def existingIp = getDataValue("ip")
|
||||||
|
def existingPort = getDataValue("port")
|
||||||
|
if (ip && ip != existingIp) {
|
||||||
|
log.debug "Updating ip from $existingIp to $ip"
|
||||||
|
updateDataValue("ip", ip)
|
||||||
|
def ipvalue = convertHexToIP(getDataValue("ip"))
|
||||||
|
sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}")
|
||||||
|
}
|
||||||
|
if (port && port != existingPort) {
|
||||||
|
log.debug "Updating port from $existingPort to $port"
|
||||||
|
updateDataValue("port", port)
|
||||||
|
}
|
||||||
subscribe("${ip}:${port}")
|
subscribe("${ip}:${port}")
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////
|
|
||||||
def resubscribe() {
|
def resubscribe() {
|
||||||
log.debug "Executing 'resubscribe()'"
|
log.debug "Executing 'resubscribe()'"
|
||||||
|
def sid = getDeviceDataByName("subscriptionId")
|
||||||
def sid = getDeviceDataByName("subscriptionId")
|
|
||||||
|
|
||||||
new physicalgraph.device.HubAction("""SUBSCRIBE /upnp/event/basicevent1 HTTP/1.1
|
new physicalgraph.device.HubAction("""SUBSCRIBE /upnp/event/basicevent1 HTTP/1.1
|
||||||
HOST: ${getHostAddress()}
|
HOST: ${getHostAddress()}
|
||||||
SID: uuid:${sid}
|
SID: uuid:${sid}
|
||||||
@@ -228,12 +229,11 @@ TIMEOUT: Second-5400
|
|||||||
|
|
||||||
|
|
||||||
""", physicalgraph.device.Protocol.LAN)
|
""", physicalgraph.device.Protocol.LAN)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////
|
|
||||||
def unsubscribe() {
|
def unsubscribe() {
|
||||||
def sid = getDeviceDataByName("subscriptionId")
|
def sid = getDeviceDataByName("subscriptionId")
|
||||||
new physicalgraph.device.HubAction("""UNSUBSCRIBE publisher path HTTP/1.1
|
new physicalgraph.device.HubAction("""UNSUBSCRIBE publisher path HTTP/1.1
|
||||||
HOST: ${getHostAddress()}
|
HOST: ${getHostAddress()}
|
||||||
SID: uuid:${sid}
|
SID: uuid:${sid}
|
||||||
@@ -242,7 +242,7 @@ SID: uuid:${sid}
|
|||||||
""", physicalgraph.device.Protocol.LAN)
|
""", physicalgraph.device.Protocol.LAN)
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////
|
|
||||||
//TODO: Use UTC Timezone
|
//TODO: Use UTC Timezone
|
||||||
def timeSyncResponse() {
|
def timeSyncResponse() {
|
||||||
log.debug "Executing 'timeSyncResponse()'"
|
log.debug "Executing 'timeSyncResponse()'"
|
||||||
@@ -267,9 +267,15 @@ User-Agent: CyberGarage-HTTP/1.0
|
|||||||
""", physicalgraph.device.Protocol.LAN)
|
""", physicalgraph.device.Protocol.LAN)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def setOffline() {
|
||||||
|
//sendEvent(name: "currentIP", value: "Offline", displayed: false)
|
||||||
|
sendEvent(name: "switch", value: "offline", descriptionText: "The device is offline")
|
||||||
|
}
|
||||||
|
|
||||||
def poll() {
|
def poll() {
|
||||||
log.debug "Executing 'poll'"
|
log.debug "Executing 'poll'"
|
||||||
|
if (device.currentValue("currentIP") != "Offline")
|
||||||
|
runIn(10, setOffline)
|
||||||
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||||
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
||||||
Content-Length: 277
|
Content-Length: 277
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ metadata {
|
|||||||
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04"
|
||||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702"
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "Z01-CIA19NAE26", deviceJoinName: "Sengled Element touch"
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45852", deviceJoinName: "GE Zigbee Plug-In Dimmer"
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45857", deviceJoinName: "GE Zigbee In-Wall Dimmer"
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ metadata {
|
|||||||
|
|
||||||
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008"
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 ON/OFF/DIM", deviceJoinName: "OSRAM LIGHTIFY LED Smart Connected Light"
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,FF00", outClusters: "0019", manufacturer: "MRVL", model: "MZ100", deviceJoinName: "Wemo Bulb"
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05", outClusters: "0019", manufacturer: "OSRAM SYLVANIA", model: "iQBR30", deviceJoinName: "Sylvania Ultra iQ"
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
|
|||||||
@@ -32,7 +32,13 @@
|
|||||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019",
|
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019",
|
||||||
manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_10T", deviceJoinName: "Kwikset 10-Button Touch Deadbolt"
|
manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_10T", deviceJoinName: "Kwikset 10-Button Touch Deadbolt"
|
||||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019",
|
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019",
|
||||||
manufacturer: "Yale", model: "YRL220 TS LL", deviceJoinName: "Yale YRL220 Lock"
|
manufacturer: "Yale", model: "YRL220 TS LL", deviceJoinName: "Yale Touch Screen Lever 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: "YRL210 PB LL", deviceJoinName: "Yale Push Button Lever Lock"
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
@@ -85,11 +91,24 @@ def uninstalled() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def configure() {
|
def configure() {
|
||||||
|
/*
|
||||||
def cmds =
|
def cmds =
|
||||||
zigbee.configSetup("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}",
|
zigbee.configSetup("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}",
|
||||||
"${TYPE_ENUM8}", 0, 3600, "{01}") +
|
"${TYPE_ENUM8}", 0, 3600, "{01}") +
|
||||||
zigbee.configSetup("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}",
|
zigbee.configSetup("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}",
|
||||||
"${TYPE_U8}", 3600, 3600, "{01}")
|
"${TYPE_U8}", 600, 21600, "{01}")
|
||||||
|
*/
|
||||||
|
def zigbeeId = device.zigbeeId
|
||||||
|
def cmds =
|
||||||
|
[
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 1 ${CLUSTER_DOORLOCK} {$zigbeeId} {}", "delay 200",
|
||||||
|
"zcl global send-me-a-report ${CLUSTER_DOORLOCK} ${DOORLOCK_ATTR_LOCKSTATE} ${TYPE_ENUM8} 0 3600 {01}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 0x${device.endpointId}", "delay 200",
|
||||||
|
|
||||||
|
"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 1 ${CLUSTER_POWER} {$zigbeeId} {}", "delay 200",
|
||||||
|
"zcl global send-me-a-report ${CLUSTER_POWER} ${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING} ${TYPE_U8} 600 21600 {01}", "delay 200",
|
||||||
|
"send 0x${device.deviceNetworkId} 1 0x${device.endpointId}", "delay 200",
|
||||||
|
]
|
||||||
log.info "configure() --- cmds: $cmds"
|
log.info "configure() --- cmds: $cmds"
|
||||||
return cmds + refresh() // send refresh cmds as part of config
|
return cmds + refresh() // send refresh cmds as part of config
|
||||||
}
|
}
|
||||||
@@ -119,13 +138,15 @@ def parse(String description) {
|
|||||||
def lock() {
|
def lock() {
|
||||||
def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_LOCK_DOOR}", "{}")
|
def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_LOCK_DOOR}", "{}")
|
||||||
log.info "lock() -- cmds: $cmds"
|
log.info "lock() -- cmds: $cmds"
|
||||||
return cmds
|
//return cmds
|
||||||
|
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_LOCK_DOOR} {}"
|
||||||
}
|
}
|
||||||
|
|
||||||
def unlock() {
|
def unlock() {
|
||||||
def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_UNLOCK_DOOR}", "{}")
|
def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_UNLOCK_DOOR}", "{}")
|
||||||
log.info "unlock() -- cmds: $cmds"
|
log.info "unlock() -- cmds: $cmds"
|
||||||
return cmds
|
//return cmds
|
||||||
|
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_UNLOCK_DOOR} {}"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private methods
|
// Private methods
|
||||||
@@ -139,8 +160,10 @@ private Map parseReportAttributeMessage(String description) {
|
|||||||
Map resultMap = [:]
|
Map resultMap = [:]
|
||||||
if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) {
|
if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) {
|
||||||
resultMap.name = "battery"
|
resultMap.name = "battery"
|
||||||
// BatteryPercentageRemaining is specified in .5% increments
|
resultMap.value = Math.round(Integer.parseInt(descMap.value, 16) / 2)
|
||||||
resultMap.value = Integer.parseInt(descMap.value, 16) / 2
|
if (device.getDataValue("manufacturer") == "Yale") { //Handling issue with Yale locks incorrect battery reporting
|
||||||
|
resultMap.value = Integer.parseInt(descMap.value, 16)
|
||||||
|
}
|
||||||
log.info "parseReportAttributeMessage() --- battery: ${resultMap.value}"
|
log.info "parseReportAttributeMessage() --- battery: ${resultMap.value}"
|
||||||
}
|
}
|
||||||
else if (descMap.clusterInt == CLUSTER_DOORLOCK && descMap.attrInt == DOORLOCK_ATTR_LOCKSTATE) {
|
else if (descMap.clusterInt == CLUSTER_DOORLOCK && descMap.attrInt == DOORLOCK_ATTR_LOCKSTATE) {
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ metadata {
|
|||||||
|
|
||||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B04"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B04"
|
||||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702"
|
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702"
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "0003, 000A,0019", manufacturer: "Jasco Products", model: "45853", deviceJoinName: "GE ZigBee Plug-In Switch"
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45856", deviceJoinName: "GE ZigBee In-Wall Switch"
|
||||||
}
|
}
|
||||||
|
|
||||||
tiles(scale: 2) {
|
tiles(scale: 2) {
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2015 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.
|
||||||
|
*
|
||||||
|
* ZigBee White Color Temperature Bulb
|
||||||
|
*
|
||||||
|
* Author: SmartThings
|
||||||
|
* Date: 2015-09-22
|
||||||
|
*/
|
||||||
|
|
||||||
|
metadata {
|
||||||
|
definition (name: "ZigBee White Color Temperature Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||||
|
|
||||||
|
capability "Actuator"
|
||||||
|
capability "Color Temperature"
|
||||||
|
capability "Configuration"
|
||||||
|
capability "Refresh"
|
||||||
|
capability "Sensor"
|
||||||
|
capability "Switch"
|
||||||
|
capability "Switch Level"
|
||||||
|
|
||||||
|
attribute "colorName", "string"
|
||||||
|
command "setGenericName"
|
||||||
|
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04", outClusters: "0019"
|
||||||
|
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY BR Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Flood BR30 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: "LIGHTIFY A19 Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Tunable White 60W"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||||
|
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||||
|
}
|
||||||
|
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||||
|
attributeState "level", action:"switch level.setLevel"
|
||||||
|
}
|
||||||
|
tileAttribute ("colorName", key: "SECONDARY_CONTROL") {
|
||||||
|
attributeState "colorName", label:'${currentValue}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||||
|
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") {
|
||||||
|
state "colorTemperature", action:"color temperature.setColorTemperature"
|
||||||
|
}
|
||||||
|
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||||
|
state "colorTemperature", label: '${currentValue} K'
|
||||||
|
}
|
||||||
|
|
||||||
|
main(["switch"])
|
||||||
|
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse incoming device messages to generate events
|
||||||
|
def parse(String description) {
|
||||||
|
log.debug "description is $description"
|
||||||
|
|
||||||
|
def finalResult = zigbee.getKnownDescription(description)
|
||||||
|
if (finalResult) {
|
||||||
|
log.info finalResult
|
||||||
|
if (finalResult.type == "update") {
|
||||||
|
log.info "$device updates: ${finalResult.value}"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sendEvent(name: finalResult.type, value: finalResult.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||||
|
log.debug zigbee.parseDescriptionAsMap(description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def off() {
|
||||||
|
zigbee.off()
|
||||||
|
}
|
||||||
|
|
||||||
|
def on() {
|
||||||
|
zigbee.on()
|
||||||
|
}
|
||||||
|
|
||||||
|
def setLevel(value) {
|
||||||
|
zigbee.setLevel(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
def refresh() {
|
||||||
|
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure() {
|
||||||
|
log.debug "Configuring Reporting and Bindings."
|
||||||
|
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
def setColorTemperature(value) {
|
||||||
|
setGenericName(value)
|
||||||
|
zigbee.setColorTemperature(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature
|
||||||
|
def setGenericName(value){
|
||||||
|
if (value != null) {
|
||||||
|
def genericName = "White"
|
||||||
|
if (value < 3300) {
|
||||||
|
genericName = "Soft White"
|
||||||
|
} else if (value < 4150) {
|
||||||
|
genericName = "Moonlight"
|
||||||
|
} else if (value <= 5000) {
|
||||||
|
genericName = "Cool White"
|
||||||
|
} else if (value >= 5000) {
|
||||||
|
genericName = "Daylight"
|
||||||
|
}
|
||||||
|
sendEvent(name: "colorName", value: genericName)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
/**
|
|
||||||
* Switch Too
|
|
||||||
*
|
|
||||||
* Copyright 2015 Bob Florian
|
|
||||||
*
|
|
||||||
* 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: "Switch Too", author: "Bob Florian") {
|
|
||||||
capability "Switch"
|
|
||||||
}
|
|
||||||
|
|
||||||
simulator {
|
|
||||||
// TODO: define status and reply messages here
|
|
||||||
}
|
|
||||||
|
|
||||||
tiles {
|
|
||||||
// TODO: define your main and details tiles here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse events into attributes
|
|
||||||
def parse(String description) {
|
|
||||||
log.debug "Parsing '${description}'"
|
|
||||||
// TODO: handle 'switch' attribute
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle commands
|
|
||||||
def on() {
|
|
||||||
log.debug "Executing 'on'"
|
|
||||||
// TODO: handle 'on' command
|
|
||||||
}
|
|
||||||
|
|
||||||
def off() {
|
|
||||||
log.debug "Executing 'off'"
|
|
||||||
// TODO: handle 'off' command
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -20,7 +20,8 @@ definition(
|
|||||||
description: "Use this free SmartApp in conjunction with the ObyThing Music app for your Mac to control and automate music and more with iTunes and SmartThings.",
|
description: "Use this free SmartApp in conjunction with the ObyThing Music app for your Mac to control and automate music and more with iTunes and SmartThings.",
|
||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
iconUrl: "http://obycode.com/obything/ObyThingSTLogo.png",
|
iconUrl: "http://obycode.com/obything/ObyThingSTLogo.png",
|
||||||
iconX2Url: "http://obycode.com/obything/ObyThingSTLogo@2x.png")
|
iconX2Url: "http://obycode.com/obything/ObyThingSTLogo@2x.png",
|
||||||
|
singleInstance: true)
|
||||||
|
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ definition(
|
|||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png",
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png",
|
||||||
oauth: true
|
oauth: true,
|
||||||
|
singleInstance: true
|
||||||
){
|
){
|
||||||
appSetting "clientId"
|
appSetting "clientId"
|
||||||
appSetting "clientSecret"
|
appSetting "clientSecret"
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ definition(
|
|||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up@2x.png",
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up@2x.png",
|
||||||
oauth: true,
|
oauth: true,
|
||||||
usePreferencesForAuthorization: false
|
usePreferencesForAuthorization: false,
|
||||||
|
singleInstance: true
|
||||||
) {
|
) {
|
||||||
appSetting "clientId"
|
appSetting "clientId"
|
||||||
appSetting "clientSecret"
|
appSetting "clientSecret"
|
||||||
appSetting "serverUrl"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
@@ -28,16 +28,13 @@ 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/callback") { action: [ GET: "callback" ] }
|
path("/oauth/callback") { action: [ GET: "callback" ] }
|
||||||
}
|
}
|
||||||
|
|
||||||
def getSmartThingsClientId() {
|
def getServerUrl() { return "https://graph.api.smartthings.com" }
|
||||||
return appSettings.clientId
|
def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" }
|
||||||
}
|
def buildRedirectUrl(page) { return buildActionUrl(page) }
|
||||||
|
|
||||||
def getSmartThingsClientSecret() {
|
|
||||||
return appSettings.clientSecret
|
|
||||||
}
|
|
||||||
|
|
||||||
def callback() {
|
def callback() {
|
||||||
def redirectUrl = null
|
def redirectUrl = null
|
||||||
@@ -63,9 +60,8 @@ def callback() {
|
|||||||
// SmartThings code, which we ignore, as we don't need to exchange for an access token.
|
// SmartThings code, which we ignore, as we don't need to exchange for an access token.
|
||||||
// Instead, go initiate the Jawbone OAuth flow.
|
// Instead, go initiate the Jawbone OAuth flow.
|
||||||
log.debug "Executing callback redirect to auth page"
|
log.debug "Executing callback redirect to auth page"
|
||||||
def stcid = getSmartThingsClientId()
|
|
||||||
state.oauthInitState = UUID.randomUUID().toString()
|
state.oauthInitState = UUID.randomUUID().toString()
|
||||||
def oauthParams = [response_type: "code", client_id: stcid, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback"]
|
def oauthParams = [response_type: "code", client_id: appSettings.clientId, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback"]
|
||||||
redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}")
|
redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -84,10 +80,11 @@ def authPage() {
|
|||||||
createAccessToken()
|
createAccessToken()
|
||||||
}
|
}
|
||||||
description = "Click to enter Jawbone Credentials"
|
description = "Click to enter Jawbone Credentials"
|
||||||
def redirectUrl = oauthInitUrl()
|
def redirectUrl = buildRedirectUrl
|
||||||
// log.debug "RedirectURL = ${redirectUrl}"
|
log.debug "RedirectURL = ${redirectUrl}"
|
||||||
return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install:false) {
|
def donebutton= state.JawboneAccessToken != null
|
||||||
section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", description:description }
|
return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install: donebutton) {
|
||||||
|
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."
|
||||||
@@ -99,17 +96,14 @@ def authPage() {
|
|||||||
|
|
||||||
def oauthInitUrl() {
|
def oauthInitUrl() {
|
||||||
log.debug "oauthInitUrl"
|
log.debug "oauthInitUrl"
|
||||||
def stcid = getSmartThingsClientId()
|
|
||||||
state.oauthInitState = UUID.randomUUID().toString()
|
state.oauthInitState = UUID.randomUUID().toString()
|
||||||
def oauthParams = [ response_type: "code", client_id: stcid, scope: "move_read sleep_read", redirect_uri: buildRedirectUrl("receiveToken") ]
|
def oauthParams = [ response_type: "code", client_id: appSettings.clientId, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback" ]
|
||||||
return "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}"
|
redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}")
|
||||||
}
|
}
|
||||||
|
|
||||||
def receiveToken(redirectUrl = null) {
|
def receiveToken(redirectUrl = null) {
|
||||||
log.debug "receiveToken"
|
log.debug "receiveToken"
|
||||||
def stcid = getSmartThingsClientId()
|
def oauthParams = [ client_id: appSettings.clientId, client_secret: appSettings.clientSecret, grant_type: "authorization_code", code: params.code ]
|
||||||
def oauthClientSecret = getSmartThingsClientSecret()
|
|
||||||
def oauthParams = [ client_id: stcid, client_secret: oauthClientSecret, grant_type: "authorization_code", code: params.code ]
|
|
||||||
def params = [
|
def params = [
|
||||||
uri: "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}",
|
uri: "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}",
|
||||||
]
|
]
|
||||||
@@ -231,18 +225,10 @@ String toQueryString(Map m) {
|
|||||||
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
||||||
}
|
}
|
||||||
|
|
||||||
def getServerUrl() { return appSettings.serverUrl ?: "https://graph.api.smartthings.com" }
|
|
||||||
|
|
||||||
def buildRedirectUrl(page) {
|
|
||||||
// log.debug "buildRedirectUrl"
|
|
||||||
// /api/token/:st_token/smartapps/installations/:id/something
|
|
||||||
return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}"
|
|
||||||
}
|
|
||||||
|
|
||||||
def validateCurrentToken() {
|
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=${getSmartThingsClientSecret()}"
|
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 ->
|
||||||
@@ -256,9 +242,7 @@ def validateCurrentToken() {
|
|||||||
if (e.statusCode == 401) { // token is expired
|
if (e.statusCode == 401) { // token is expired
|
||||||
log.debug "Access token is expired"
|
log.debug "Access token is expired"
|
||||||
if (state.refreshToken) { // if we have this we are okay
|
if (state.refreshToken) { // if we have this we are okay
|
||||||
def stcid = getSmartThingsClientId()
|
def oauthParams = [client_id: appSettings.clientId, client_secret: appSettings.clientSecret, grant_type: "refresh_token", refresh_token: state.refreshToken]
|
||||||
def oauthClientSecret = getSmartThingsClientSecret()
|
|
||||||
def oauthParams = [client_id: stcid, client_secret: oauthClientSecret, grant_type: "refresh_token", refresh_token: state.refreshToken]
|
|
||||||
def tokenUrl = "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}"
|
def tokenUrl = "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}"
|
||||||
def params = [
|
def params = [
|
||||||
uri: tokenUrl
|
uri: tokenUrl
|
||||||
@@ -287,9 +271,10 @@ def validateCurrentToken() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def initialize() {
|
def initialize() {
|
||||||
def hookUrl = "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback"
|
log.debug "Callback URL - Webhook"
|
||||||
def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl"
|
def localServerUrl = getApiServerUrl()
|
||||||
log.debug "Callback URL: $webhook"
|
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"
|
||||||
httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ])
|
httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,7 +312,6 @@ def setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def installed() {
|
def installed() {
|
||||||
enableCallback()
|
|
||||||
|
|
||||||
if (!state.accessToken) {
|
if (!state.accessToken) {
|
||||||
log.debug "About to create access token"
|
log.debug "About to create access token"
|
||||||
@@ -340,7 +324,6 @@ def installed() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def updated() {
|
def updated() {
|
||||||
enableCallback()
|
|
||||||
|
|
||||||
if (!state.accessToken) {
|
if (!state.accessToken) {
|
||||||
log.debug "About to create access token"
|
log.debug "About to create access token"
|
||||||
@@ -499,4 +482,4 @@ def hookEventHandler() {
|
|||||||
|
|
||||||
def html = """{"code":200,"message":"OK"}"""
|
def html = """{"code":200,"message":"OK"}"""
|
||||||
render contentType: 'application/json', data: html
|
render contentType: 'application/json', data: html
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
/**
|
|
||||||
*
|
|
||||||
* Lights On When Door Open After Sundown
|
|
||||||
*
|
|
||||||
* Based on "Turn It On When It Opens" by SmartThings
|
|
||||||
*
|
|
||||||
* Author: Aaron Crocco
|
|
||||||
*/
|
|
||||||
preferences {
|
|
||||||
section("When the door opens..."){
|
|
||||||
input "contact1", "capability.contactSensor", title: "Where?"
|
|
||||||
}
|
|
||||||
section("Turn on these lights..."){
|
|
||||||
input "switches", "capability.switch", multiple: true
|
|
||||||
}
|
|
||||||
section("and change mode to...") {
|
|
||||||
input "HomeAfterDarkMode", "mode", title: "Mode?"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def installed()
|
|
||||||
{
|
|
||||||
subscribe(contact1, "contact.open", contactOpenHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
def updated()
|
|
||||||
{
|
|
||||||
unsubscribe()
|
|
||||||
subscribe(contact1, "contact.open", contactOpenHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
def contactOpenHandler(evt) {
|
|
||||||
log.debug "$evt.value: $evt, $settings"
|
|
||||||
|
|
||||||
//Check current time to see if it's after sundown.
|
|
||||||
def s = getSunriseAndSunset(zipCode: zipCode, sunriseOffset: sunriseOffset, sunsetOffset: sunsetOffset)
|
|
||||||
def now = new Date()
|
|
||||||
def setTime = s.sunset
|
|
||||||
log.debug "Sunset is at $setTime. Current time is $now"
|
|
||||||
|
|
||||||
|
|
||||||
if (setTime.before(now)) { //Executes only if it's after sundown.
|
|
||||||
|
|
||||||
log.trace "Turning on switches: $switches"
|
|
||||||
switches.on()
|
|
||||||
log.trace "Changing house mode to $HomeAfterDarkMode"
|
|
||||||
setLocationMode(HomeAfterDarkMode)
|
|
||||||
sendPush("Welcome home! Changing mode to $HomeAfterDarkMode.")
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -26,7 +26,8 @@ definition(
|
|||||||
iconUrl: "http://i.imgur.com/HU0ANBp.png",
|
iconUrl: "http://i.imgur.com/HU0ANBp.png",
|
||||||
iconX2Url: "http://i.imgur.com/HU0ANBp.png",
|
iconX2Url: "http://i.imgur.com/HU0ANBp.png",
|
||||||
iconX3Url: "http://i.imgur.com/HU0ANBp.png",
|
iconX3Url: "http://i.imgur.com/HU0ANBp.png",
|
||||||
oauth: true)
|
oauth: true,
|
||||||
|
singleInstance: true)
|
||||||
|
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
definition(
|
definition(
|
||||||
name: "Smart Lock / Unlock",
|
name: "Smart Auto Lock / Unlock",
|
||||||
namespace: "",
|
namespace: "smart-auto-lock-unlock",
|
||||||
author: "Arnaud",
|
author: "Arnaud",
|
||||||
description: "Automatically locks door X minutes after being closed and keeps door unlocked if door is open.",
|
description: "Automatically locks door X minutes after being closed and keeps door unlocked if door is open.",
|
||||||
category: "Safety & Security",
|
category: "Safety & Security",
|
||||||
@@ -21,7 +21,8 @@
|
|||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
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",
|
||||||
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png"
|
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
||||||
|
singleInstance: true
|
||||||
)
|
)
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ definition(
|
|||||||
description: "Connect your Ecobee thermostat to SmartThings.",
|
description: "Connect your Ecobee thermostat to SmartThings.",
|
||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png"
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png",
|
||||||
|
singleInstance: true
|
||||||
) {
|
) {
|
||||||
appSetting "clientId"
|
appSetting "clientId"
|
||||||
appSetting "serverUrl"
|
appSetting "serverUrl"
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ definition(
|
|||||||
description: "Connect and take pictures using your Foscam camera from inside the Smartthings app.",
|
description: "Connect and take pictures using your Foscam camera from inside the Smartthings app.",
|
||||||
category: "SmartThings Internal",
|
category: "SmartThings Internal",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam@2x.png"
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam@2x.png",
|
||||||
|
singleInstance: true
|
||||||
)
|
)
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ definition(
|
|||||||
description: "Allows you to connect your Philips Hue lights with SmartThings and control them from your Things area or Dashboard in the SmartThings Mobile app. Adjust colors by going to the Thing detail screen for your Hue lights (tap the gear on Hue tiles).\n\nPlease update your Hue Bridge first, outside of the SmartThings app, using the Philips Hue app.",
|
description: "Allows you to connect your Philips Hue lights with SmartThings and control them from your Things area or Dashboard in the SmartThings Mobile app. Adjust colors by going to the Thing detail screen for your Hue lights (tap the gear on Hue tiles).\n\nPlease update your Hue Bridge first, outside of the SmartThings app, using the Philips Hue app.",
|
||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png"
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png",
|
||||||
|
singleInstance: true
|
||||||
)
|
)
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
@@ -143,7 +144,7 @@ def bulbDiscovery() {
|
|||||||
if (numFound == 0)
|
if (numFound == 0)
|
||||||
app.updateSetting("selectedBulbs", "")
|
app.updateSetting("selectedBulbs", "")
|
||||||
|
|
||||||
if((bulbRefreshCount % 3) == 0) {
|
if((bulbRefreshCount % 5) == 0) {
|
||||||
discoverHueBulbs()
|
discoverHueBulbs()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,11 +319,15 @@ def addBulbs() {
|
|||||||
def newHueBulb
|
def newHueBulb
|
||||||
if (bulbs instanceof java.util.Map) {
|
if (bulbs instanceof java.util.Map) {
|
||||||
newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
|
newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
|
||||||
if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light")) {
|
if (newHueBulb != null) {
|
||||||
d = addChildDevice("smartthings", "Hue Lux Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light") ) {
|
||||||
} else {
|
d = addChildDevice("smartthings", "Hue Lux Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
||||||
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
} else {
|
||||||
}
|
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.debug "$dni in not longer paired to the Hue Bridge or ID changed"
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
//backwards compatable
|
//backwards compatable
|
||||||
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
|
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
|
||||||
@@ -604,23 +609,22 @@ def parse(childDevice, description) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def on(childDevice, transition_deprecated = 0) {
|
def on(childDevice) {
|
||||||
log.debug "Executing 'on'"
|
log.debug "Executing 'on'"
|
||||||
def percent = childDevice.device?.currentValue("level") as Integer
|
put("lights/${getId(childDevice)}/state", [on: true])
|
||||||
def level = Math.min(Math.round(percent * 255 / 100), 255)
|
return "Bulb is On"
|
||||||
put("lights/${getId(childDevice)}/state", [bri: level, on: true])
|
|
||||||
return "level: $percent"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def off(childDevice, transition_deprecated = 0) {
|
def off(childDevice) {
|
||||||
log.debug "Executing 'off'"
|
log.debug "Executing 'off'"
|
||||||
put("lights/${getId(childDevice)}/state", [on: false])
|
put("lights/${getId(childDevice)}/state", [on: false])
|
||||||
return "level: 0"
|
return "Bulb is Off"
|
||||||
}
|
}
|
||||||
|
|
||||||
def setLevel(childDevice, percent) {
|
def setLevel(childDevice, percent) {
|
||||||
log.debug "Executing 'setLevel'"
|
log.debug "Executing 'setLevel'"
|
||||||
def level = Math.min(Math.round(percent * 255 / 100), 255)
|
def level
|
||||||
|
if (percent == 1) level = 1 else level = Math.min(Math.round(percent * 255 / 100), 255)
|
||||||
put("lights/${getId(childDevice)}/state", [bri: level, on: percent > 0])
|
put("lights/${getId(childDevice)}/state", [bri: level, on: percent > 0])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,7 +640,7 @@ def setHue(childDevice, percent) {
|
|||||||
put("lights/${getId(childDevice)}/state", [hue: level])
|
put("lights/${getId(childDevice)}/state", [hue: level])
|
||||||
}
|
}
|
||||||
|
|
||||||
def setColor(childDevice, huesettings, alert_deprecated = "", transition_deprecated = 0) {
|
def setColor(childDevice, huesettings) {
|
||||||
log.debug "Executing 'setColor($huesettings)'"
|
log.debug "Executing 'setColor($huesettings)'"
|
||||||
def hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
|
def hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
|
||||||
def sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255)
|
def sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255)
|
||||||
@@ -645,7 +649,7 @@ def setColor(childDevice, huesettings, alert_deprecated = "", transition_depreca
|
|||||||
|
|
||||||
def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition]
|
def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition]
|
||||||
if (huesettings.level != null) {
|
if (huesettings.level != null) {
|
||||||
value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
|
if (huesettings.level == 1) value.bri = 1 else value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
|
||||||
value.on = value.bri > 0
|
value.on = value.bri > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -723,8 +727,6 @@ private getBridgeIP() {
|
|||||||
def serialNumber = selectedHue
|
def serialNumber = selectedHue
|
||||||
def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value
|
def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value
|
||||||
if (!bridge) {
|
if (!bridge) {
|
||||||
//failed because mac address sent from hub is wrong and doesn't match the hue's real mac address and serial number
|
|
||||||
//in this case we will look up the bridge by comparing the incorrect mac addresses
|
|
||||||
bridge = getHueBridges().find { it?.value?.mac?.equalsIgnoreCase(serialNumber) }?.value
|
bridge = getHueBridges().find { it?.value?.mac?.equalsIgnoreCase(serialNumber) }?.value
|
||||||
}
|
}
|
||||||
if (bridge?.ip && bridge?.port) {
|
if (bridge?.ip && bridge?.port) {
|
||||||
|
|||||||
@@ -98,6 +98,15 @@ def installed() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def updated() {
|
def updated() {
|
||||||
|
def currentDeviceIds = settings.collect { k, devices -> devices }.flatten().collect { it.id }.unique()
|
||||||
|
def subscriptionDevicesToRemove = app.subscriptions*.device.findAll { device ->
|
||||||
|
!currentDeviceIds.contains(device.id)
|
||||||
|
}
|
||||||
|
subscriptionDevicesToRemove.each { device ->
|
||||||
|
log.debug "Removing $device.displayName subscription"
|
||||||
|
state.remove(device.id)
|
||||||
|
unsubscribe(device)
|
||||||
|
}
|
||||||
log.debug settings
|
log.debug settings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ definition(
|
|||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/life360.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/life360.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/life360@2x.png",
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/life360@2x.png",
|
||||||
oauth: [displayName: "Life360", displayLink: "Life360"]
|
oauth: [displayName: "Life360", displayLink: "Life360"],
|
||||||
|
singleInstance: true
|
||||||
) {
|
) {
|
||||||
appSetting "clientId"
|
appSetting "clientId"
|
||||||
appSetting "clientSecret"
|
appSetting "clientSecret"
|
||||||
|
|||||||
@@ -5,22 +5,23 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
definition(
|
definition(
|
||||||
name: "LIFX (Connect)",
|
name: "LIFX (Connect)",
|
||||||
namespace: "smartthings",
|
namespace: "smartthings",
|
||||||
author: "LIFX",
|
author: "LIFX",
|
||||||
description: "Allows you to use LIFX smart light bulbs with SmartThings.",
|
description: "Allows you to use LIFX smart light bulbs with SmartThings.",
|
||||||
category: "Convenience",
|
category: "Convenience",
|
||||||
iconUrl: "https://cloud.lifx.com/images/lifx.png",
|
iconUrl: "https://cloud.lifx.com/images/lifx.png",
|
||||||
iconX2Url: "https://cloud.lifx.com/images/lifx.png",
|
iconX2Url: "https://cloud.lifx.com/images/lifx.png",
|
||||||
iconX3Url: "https://cloud.lifx.com/images/lifx.png",
|
iconX3Url: "https://cloud.lifx.com/images/lifx.png",
|
||||||
oauth: true) {
|
oauth: true,
|
||||||
appSetting "clientId"
|
singleInstance: true) {
|
||||||
appSetting "clientSecret"
|
appSetting "clientId"
|
||||||
}
|
appSetting "clientSecret"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
page(name: "Credentials", title: "LIFX", content: "authPage", install: false)
|
page(name: "Credentials", title: "LIFX", content: "authPage", install: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
mappings {
|
mappings {
|
||||||
@@ -32,29 +33,29 @@ mappings {
|
|||||||
path("/test") { action: [ GET: "oauthSuccess" ] }
|
path("/test") { action: [ GET: "oauthSuccess" ] }
|
||||||
}
|
}
|
||||||
|
|
||||||
def getServerUrl() { return "https://graph.api.smartthings.com" }
|
def getServerUrl() { return "https://graph.api.smartthings.com" }
|
||||||
def apiURL(path = '/') { return "https://api.lifx.com/v1beta1${path}" }
|
def getCallbackUrl() { return "https://graph.api.smartthings.com/oauth/callback"}
|
||||||
def buildRedirectUrl(page) {
|
def apiURL(path = '/') { return "https://api.lifx.com/v1${path}" }
|
||||||
return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}"
|
def getSecretKey() { return appSettings.secretKey }
|
||||||
}
|
def getClientId() { return appSettings.clientId }
|
||||||
|
|
||||||
def authPage() {
|
def authPage() {
|
||||||
log.debug "authPage"
|
log.debug "authPage test1"
|
||||||
if (!state.lifxAccessToken) {
|
if (!state.lifxAccessToken) {
|
||||||
log.debug "no LIFX access token"
|
log.debug "no LIFX access token"
|
||||||
// This is the SmartThings access token
|
// This is the SmartThings access token
|
||||||
if (!state.accessToken) {
|
if (!state.accessToken) {
|
||||||
log.debug "no access token, create access token"
|
log.debug "no access token, create access token"
|
||||||
createAccessToken() // predefined method
|
state.accessToken = createAccessToken() // predefined method
|
||||||
}
|
}
|
||||||
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}" // this triggers oauthInit() below
|
def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" // this triggers oauthInit() below
|
||||||
log.debug "app id: ${app.id}"
|
// def redirectUrl = "${apiServerUrl}"
|
||||||
|
log.debug "app id: ${app.id}"
|
||||||
log.debug "redirect url: ${redirectUrl}"
|
log.debug "redirect url: ${redirectUrl}"
|
||||||
return dynamicPage(name: "Credentials", title: "Connect to LIFX", nextPage: null, uninstall: true, install:false) {
|
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")
|
||||||
// href(url:buildRedirectUrl("test"), title: "Message test")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -62,17 +63,15 @@ def authPage() {
|
|||||||
|
|
||||||
def options = locationOptions() ?: []
|
def options = locationOptions() ?: []
|
||||||
def count = options.size()
|
def count = options.size()
|
||||||
def refreshInterval = 3
|
|
||||||
|
|
||||||
return dynamicPage(name:"Credentials", title:"Select devices...", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
return dynamicPage(name:"Credentials", title:"", nextPage:"", install:true, uninstall: true) {
|
||||||
section("Select your location") {
|
section("Select your location") {
|
||||||
input "selectedLocationId", "enum", required:true, title:"Select location (${count} found)", multiple:false, options:options
|
input "selectedLocationId", "enum", required:true, title:"Select location (${count} found)", multiple:false, options:options, submitOnChange: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// OAuth
|
// OAuth
|
||||||
|
|
||||||
def oauthInit() {
|
def oauthInit() {
|
||||||
@@ -111,7 +110,7 @@ def oauthCallback() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def oauthReceiveToken(redirectUrl = null) {
|
def oauthReceiveToken(redirectUrl = null) {
|
||||||
|
// Not sure what redirectUrl is for
|
||||||
log.debug "receiveToken - params: ${params}"
|
log.debug "receiveToken - params: ${params}"
|
||||||
def oauthParams = [ client_id: "${appSettings.clientId}", client_secret: "${appSettings.clientSecret}", grant_type: "authorization_code", code: params.code, scope: params.scope ] // how is params.code valid here?
|
def oauthParams = [ client_id: "${appSettings.clientId}", client_secret: "${appSettings.clientSecret}", grant_type: "authorization_code", code: params.code, scope: params.scope ] // how is params.code valid here?
|
||||||
def params = [
|
def params = [
|
||||||
@@ -134,25 +133,25 @@ def oauthReceiveToken(redirectUrl = null) {
|
|||||||
|
|
||||||
def oauthSuccess() {
|
def oauthSuccess() {
|
||||||
def message = """
|
def message = """
|
||||||
<p>Your LIFX Account is now connected to SmartThings!</p>
|
<p>Your LIFX Account is now connected to SmartThings!</p>
|
||||||
<p>Click 'Done' to finish setup.</p>
|
<p>Click 'Done' to finish setup.</p>
|
||||||
"""
|
"""
|
||||||
oauthConnectionStatus(message)
|
oauthConnectionStatus(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
def oauthFailure() {
|
def oauthFailure() {
|
||||||
def message = """
|
def message = """
|
||||||
<p>The connection could not be established!</p>
|
<p>The connection could not be established!</p>
|
||||||
<p>Click 'Done' to return to the menu.</p>
|
<p>Click 'Done' to return to the menu.</p>
|
||||||
"""
|
"""
|
||||||
oauthConnectionStatus(message)
|
oauthConnectionStatus(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
def oauthReceivedToken() {
|
def oauthReceivedToken() {
|
||||||
def message = """
|
def message = """
|
||||||
<p>Your LIFX Account is already connected to SmartThings!</p>
|
<p>Your LIFX Account is already connected to SmartThings!</p>
|
||||||
<p>Click 'Done' to finish setup.</p>
|
<p>Click 'Done' to finish setup.</p>
|
||||||
"""
|
"""
|
||||||
oauthConnectionStatus(message)
|
oauthConnectionStatus(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,74 +159,74 @@ def oauthConnectionStatus(message, redirectUrl = null) {
|
|||||||
def redirectHtml = ""
|
def redirectHtml = ""
|
||||||
if (redirectUrl) {
|
if (redirectUrl) {
|
||||||
redirectHtml = """
|
redirectHtml = """
|
||||||
<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>
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width">
|
<meta name="viewport" content="width=device-width">
|
||||||
<title>SmartThings Connection</title>
|
<title>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: 280;
|
width: 280;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
img {
|
img {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
img:nth-child(2) {
|
img:nth-child(2) {
|
||||||
margin: 0 15px;
|
margin: 0 15px;
|
||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
font-size: 1.2em;
|
font-size: 1.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 20px;
|
padding: 0 20px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
span {
|
span {
|
||||||
font-family: 'Swiss 721 W01 Light';
|
font-family: 'Swiss 721 W01 Light';
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
${redirectHtml}
|
${redirectHtml}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<img src='https://cloud.lifx.com/images/lifx.png' alt='LIFX icon' width='100'/>
|
<img src='https://cloud.lifx.com/images/lifx.png' alt='LIFX icon' width='100'/>
|
||||||
<img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png' alt='connected device icon' width="40"/>
|
<img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png' alt='connected device icon' width="40"/>
|
||||||
<img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png' alt='SmartThings logo' width="100"/>
|
<img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png' alt='SmartThings logo' width="100"/>
|
||||||
<p>
|
<p>
|
||||||
${message}
|
${message}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
render contentType: 'text/html', data: html
|
render contentType: 'text/html', data: html
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +237,6 @@ String toQueryString(Map m) {
|
|||||||
// App lifecycle hooks
|
// App lifecycle hooks
|
||||||
|
|
||||||
def installed() {
|
def installed() {
|
||||||
enableCallback() // wtf does this do?
|
|
||||||
if (!state.accessToken) {
|
if (!state.accessToken) {
|
||||||
createAccessToken()
|
createAccessToken()
|
||||||
} else {
|
} else {
|
||||||
@@ -250,7 +248,6 @@ def installed() {
|
|||||||
|
|
||||||
// called after settings are changed
|
// called after settings are changed
|
||||||
def updated() {
|
def updated() {
|
||||||
enableCallback() // not sure what this does
|
|
||||||
if (!state.accessToken) {
|
if (!state.accessToken) {
|
||||||
createAccessToken()
|
createAccessToken()
|
||||||
} else {
|
} else {
|
||||||
@@ -304,27 +301,36 @@ def logErrors(options = [errorReturn: null, logObject: log], Closure c) {
|
|||||||
state.remove("lifxAccessToken")
|
state.remove("lifxAccessToken")
|
||||||
options.logObject.warn "Access token is not valid"
|
options.logObject.warn "Access token is not valid"
|
||||||
}
|
}
|
||||||
return options.errerReturn
|
return options.errorReturn
|
||||||
} catch (java.net.SocketTimeoutException e) {
|
} catch (java.net.SocketTimeoutException e) {
|
||||||
options.logObject.warn "Connection timed out, not much we can do here"
|
options.logObject.warn "Connection timed out, not much we can do here"
|
||||||
return options.errerReturn
|
return options.errorReturn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def apiGET(path) {
|
def apiGET(path) {
|
||||||
httpGet(uri: apiURL(path), headers: apiRequestHeaders()) {response ->
|
try {
|
||||||
logResponse(response)
|
httpGet(uri: apiURL(path), headers: apiRequestHeaders()) {response ->
|
||||||
return response
|
logResponse(response)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
} catch (groovyx.net.http.HttpResponseException e) {
|
||||||
|
logResponse(e.response)
|
||||||
|
return e.response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def apiPUT(path, body = [:]) {
|
def apiPUT(path, body = [:]) {
|
||||||
log.debug("Beginning API PUT: ${path}, ${body}")
|
try {
|
||||||
httpPutJson(uri: apiURL(path), body: new groovy.json.JsonBuilder(body).toString(), headers: apiRequestHeaders(), ) {response ->
|
log.debug("Beginning API PUT: ${path}, ${body}")
|
||||||
logResponse(response)
|
httpPutJson(uri: apiURL(path), body: new groovy.json.JsonBuilder(body).toString(), headers: apiRequestHeaders(), ) {response ->
|
||||||
return response
|
logResponse(response)
|
||||||
}
|
return response
|
||||||
}
|
}
|
||||||
|
} catch (groovyx.net.http.HttpResponseException e) {
|
||||||
|
logResponse(e.response)
|
||||||
|
return e.response
|
||||||
|
}}
|
||||||
|
|
||||||
def devicesList(selector = '') {
|
def devicesList(selector = '') {
|
||||||
logErrors([]) {
|
logErrors([]) {
|
||||||
@@ -339,12 +345,12 @@ def devicesList(selector = '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Map locationOptions() {
|
Map locationOptions() {
|
||||||
|
|
||||||
def options = [:]
|
def options = [:]
|
||||||
def devices = devicesList()
|
def devices = devicesList()
|
||||||
devices.each { device ->
|
devices.each { device ->
|
||||||
options[device.location.id] = device.location.name
|
options[device.location.id] = device.location.name
|
||||||
}
|
}
|
||||||
|
log.debug("Locations: ${options}")
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,28 +364,32 @@ def updateDevices() {
|
|||||||
state.devices = [:]
|
state.devices = [:]
|
||||||
}
|
}
|
||||||
def devices = devicesInLocation()
|
def devices = devicesInLocation()
|
||||||
def deviceIds = devices*.id
|
def selectors = []
|
||||||
|
|
||||||
|
log.debug("All selectors: ${selectors}")
|
||||||
|
|
||||||
devices.each { device ->
|
devices.each { device ->
|
||||||
def childDevice = getChildDevice(device.id)
|
def childDevice = getChildDevice(device.id)
|
||||||
|
selectors.add("${device.id}")
|
||||||
if (!childDevice) {
|
if (!childDevice) {
|
||||||
log.info("Adding device ${device.id}: ${device.capabilities}")
|
log.info("Adding device ${device.id}: ${device.product}")
|
||||||
def data = [
|
def data = [
|
||||||
label: device.label,
|
label: device.label,
|
||||||
level: sprintf("%f", (device.brightness ?: 1) * 100),
|
level: Math.round((device.brightness ?: 1) * 100),
|
||||||
switch: device.connected ? device.power : "unreachable",
|
switch: device.connected ? device.power : "unreachable",
|
||||||
colorTemperature: device.color.kelvin
|
colorTemperature: device.color.kelvin
|
||||||
]
|
]
|
||||||
if (device.capabilities.has_color) {
|
if (device.product.capabilities.has_color) {
|
||||||
data["color"] = colorUtil.hslToHex((device.color.hue / 3.6) as int, (device.color.saturation * 100) as int)
|
data["color"] = colorUtil.hslToHex((device.color.hue / 3.6) as int, (device.color.saturation * 100) as int)
|
||||||
data["hue"] = device.color.hue / 3.6
|
data["hue"] = device.color.hue / 3.6
|
||||||
data["saturation"] = device.color.saturation * 100
|
data["saturation"] = device.color.saturation * 100
|
||||||
childDevice = addChildDevice("smartthings", "LIFX Color Bulb", device.id, null, data)
|
childDevice = addChildDevice(app.namespace, "LIFX Color Bulb", device.id, null, data)
|
||||||
} else {
|
} else {
|
||||||
childDevice = addChildDevice("smartthings", "LIFX White Bulb", device.id, null, data)
|
childDevice = addChildDevice(app.namespace, "LIFX White Bulb", device.id, null, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getChildDevices().findAll { !deviceIds.contains(it.deviceNetworkId) }.each {
|
getChildDevices().findAll { !selectors.contains("${it.deviceNetworkId}") }.each {
|
||||||
log.info("Deleting ${it.deviceNetworkId}")
|
log.info("Deleting ${it.deviceNetworkId}")
|
||||||
deleteChildDevice(it.deviceNetworkId)
|
deleteChildDevice(it.deviceNetworkId)
|
||||||
}
|
}
|
||||||
@@ -391,4 +401,4 @@ def refreshDevices() {
|
|||||||
getChildDevices().each { device ->
|
getChildDevices().each { device ->
|
||||||
device.refresh()
|
device.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
* locks | lock | lock, unlock | locked, unlocked
|
* locks | lock | lock, unlock | locked, unlocked
|
||||||
* ---------------------+-------------------+-----------------------------+------------------------------------
|
* ---------------------+-------------------+-----------------------------+------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
definition(
|
definition(
|
||||||
name: "Logitech Harmony (Connect)",
|
name: "Logitech Harmony (Connect)",
|
||||||
namespace: "smartthings",
|
namespace: "smartthings",
|
||||||
@@ -43,11 +43,11 @@ definition(
|
|||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony%402x.png",
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony%402x.png",
|
||||||
oauth: [displayName: "Logitech Harmony", displayLink: "http://www.logitech.com/en-us/harmony-remotes"]
|
oauth: [displayName: "Logitech Harmony", displayLink: "http://www.logitech.com/en-us/harmony-remotes"],
|
||||||
|
singleInstance: true
|
||||||
){
|
){
|
||||||
appSetting "clientId"
|
appSetting "clientId"
|
||||||
appSetting "clientSecret"
|
appSetting "clientSecret"
|
||||||
appSetting "callbackUrl"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
preferences(oauthPage: "deviceAuthorization") {
|
preferences(oauthPage: "deviceAuthorization") {
|
||||||
@@ -89,16 +89,18 @@ mappings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def getServerUrl() { return "https://graph.api.smartthings.com" }
|
def getServerUrl() { return "https://graph.api.smartthings.com" }
|
||||||
|
def getCallbackUrl() { "https://graph.api.smartthings.com/oauth/callback" }
|
||||||
|
def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" }
|
||||||
|
|
||||||
def authPage() {
|
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 "About to create access token"
|
||||||
createAccessToken()
|
createAccessToken()
|
||||||
}
|
}
|
||||||
description = "Click to enter Harmony Credentials"
|
description = "Click to enter Harmony Credentials"
|
||||||
def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}"
|
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 { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description }
|
||||||
}
|
}
|
||||||
@@ -110,7 +112,7 @@ def authPage() {
|
|||||||
|
|
||||||
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) {
|
||||||
@@ -120,13 +122,14 @@ def authPage() {
|
|||||||
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, options:huboptions
|
||||||
}
|
}
|
||||||
if (numFoundHub > 0 && numFoundAct > 0 && false)
|
// Virtual activity flag
|
||||||
|
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, 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.") {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +141,7 @@ def callback() {
|
|||||||
} else {
|
} else {
|
||||||
log.warn "No authQueryString"
|
log.warn "No authQueryString"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.HarmonyAccessToken) {
|
if (state.HarmonyAccessToken) {
|
||||||
log.debug "Access token already exists"
|
log.debug "Access token already exists"
|
||||||
discovery()
|
discovery()
|
||||||
@@ -163,8 +166,8 @@ def callback() {
|
|||||||
|
|
||||||
def init() {
|
def init() {
|
||||||
log.debug "Requesting Code"
|
log.debug "Requesting Code"
|
||||||
def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${appSettings.callbackUrl}" ]
|
def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${callbackUrl}" ]
|
||||||
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) {
|
||||||
@@ -174,7 +177,7 @@ def receiveToken(redirectUrl = null) {
|
|||||||
uri: "https://home.myharmony.com/oauth2/token?${toQueryString(oauthParams)}",
|
uri: "https://home.myharmony.com/oauth2/token?${toQueryString(oauthParams)}",
|
||||||
]
|
]
|
||||||
try {
|
try {
|
||||||
httpPost(params) { response ->
|
httpPost(params) { response ->
|
||||||
state.HarmonyAccessToken = response.data.access_token
|
state.HarmonyAccessToken = response.data.access_token
|
||||||
}
|
}
|
||||||
} catch (java.util.concurrent.TimeoutException e) {
|
} catch (java.util.concurrent.TimeoutException e) {
|
||||||
@@ -221,7 +224,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>
|
||||||
@@ -302,30 +305,28 @@ def buildRedirectUrl(page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def installed() {
|
def installed() {
|
||||||
enableCallback()
|
|
||||||
if (!state.accessToken) {
|
if (!state.accessToken) {
|
||||||
log.debug "About to create access token"
|
log.debug "About to create access token"
|
||||||
createAccessToken()
|
createAccessToken()
|
||||||
} else {
|
} else {
|
||||||
initialize()
|
initialize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def updated() {
|
def updated() {
|
||||||
unsubscribe()
|
unsubscribe()
|
||||||
unschedule()
|
unschedule()
|
||||||
enableCallback()
|
|
||||||
if (!state.accessToken) {
|
if (!state.accessToken) {
|
||||||
log.debug "About to create access token"
|
log.debug "About to create access token"
|
||||||
createAccessToken()
|
createAccessToken()
|
||||||
} else {
|
} else {
|
||||||
initialize()
|
initialize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def uninstalled() {
|
def uninstalled() {
|
||||||
if (state.HarmonyAccessToken) {
|
if (state.HarmonyAccessToken) {
|
||||||
try {
|
try {
|
||||||
state.HarmonyAccessToken = ""
|
state.HarmonyAccessToken = ""
|
||||||
log.debug "Success disconnecting Harmony from SmartThings"
|
log.debug "Success disconnecting Harmony from SmartThings"
|
||||||
} catch (groovyx.net.http.HttpResponseException e) {
|
} catch (groovyx.net.http.HttpResponseException e) {
|
||||||
@@ -339,7 +340,7 @@ def initialize() {
|
|||||||
if (selectedhubs || selectedactivities) {
|
if (selectedhubs || selectedactivities) {
|
||||||
addDevice()
|
addDevice()
|
||||||
runEvery5Minutes("discovery")
|
runEvery5Minutes("discovery")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def getHarmonydevices() {
|
def getHarmonydevices() {
|
||||||
@@ -359,20 +360,20 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.HarmonyHubs = hubs
|
state.HarmonyHubs = hubs
|
||||||
state.HarmonyActivities = activities
|
state.HarmonyActivities = activities
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//CHILD DEVICE METHODS
|
//CHILD DEVICE METHODS
|
||||||
def discovery() {
|
def discovery() {
|
||||||
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)}"
|
||||||
try {
|
try {
|
||||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||||
@@ -384,11 +385,11 @@ def discovery() {
|
|||||||
poll()
|
poll()
|
||||||
} else {
|
} else {
|
||||||
log.debug "Error: $response.status"
|
log.debug "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 Access token has expired"
|
||||||
}
|
}
|
||||||
} catch (java.net.SocketTimeoutException e) {
|
} catch (java.net.SocketTimeoutException e) {
|
||||||
@@ -396,12 +397,12 @@ def discovery() {
|
|||||||
state.resethub = true
|
state.resethub = true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.warn "Hostname in certificate didn't match. Please try again later."
|
log.warn "Hostname in certificate didn't match. Please try again later."
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
def addDevice() {
|
def addDevice() {
|
||||||
log.trace "Adding Hubs"
|
log.trace "Adding Hubs"
|
||||||
selectedhubs.each { dni ->
|
selectedhubs.each { dni ->
|
||||||
def d = getChildDevice(dni)
|
def d = getChildDevice(dni)
|
||||||
if(!d) {
|
if(!d) {
|
||||||
@@ -412,8 +413,8 @@ def addDevice() {
|
|||||||
} else {
|
} else {
|
||||||
log.trace "found ${d.displayName} with id $dni already exists"
|
log.trace "found ${d.displayName} with id $dni already exists"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.trace "Adding Activities"
|
log.trace "Adding Activities"
|
||||||
selectedactivities.each { dni ->
|
selectedactivities.each { dni ->
|
||||||
def d = getChildDevice(dni)
|
def d = getChildDevice(dni)
|
||||||
if(!d) {
|
if(!d) {
|
||||||
@@ -424,7 +425,7 @@ def addDevice() {
|
|||||||
} else {
|
} else {
|
||||||
log.trace "found ${d.displayName} with id $dni already exists"
|
log.trace "found ${d.displayName} with id $dni already exists"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def activity(dni,mode) {
|
def activity(dni,mode) {
|
||||||
@@ -432,26 +433,26 @@ def activity(dni,mode) {
|
|||||||
def msg = "Command failed"
|
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(Params)}"
|
||||||
} 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(Params)}"
|
||||||
} 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(Params)}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
httpPostJson(uri: url) { response ->
|
httpPostJson(uri: url) { response ->
|
||||||
if (response.data.code == 200 || dni == "all") {
|
if (response.data.code == 200 || dni == "all") {
|
||||||
msg = "Command sent succesfully"
|
msg = "Command sent succesfully"
|
||||||
state.aux = 0
|
state.aux = 0
|
||||||
} else {
|
} else {
|
||||||
msg = "Command failed. Error: $response.data.code"
|
msg = "Command failed. Error: $response.data.code"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (groovyx.net.http.HttpResponseException ex) {
|
} catch (groovyx.net.http.HttpResponseException ex) {
|
||||||
log.error ex
|
log.error ex
|
||||||
if (state.aux == 0) {
|
if (state.aux == 0) {
|
||||||
@@ -459,7 +460,7 @@ def activity(dni,mode) {
|
|||||||
activity(dni,mode)
|
activity(dni,mode)
|
||||||
} else {
|
} else {
|
||||||
msg = ex
|
msg = ex
|
||||||
state.aux = 0
|
state.aux = 0
|
||||||
}
|
}
|
||||||
} catch(Exception ex) {
|
} catch(Exception ex) {
|
||||||
msg = ex
|
msg = ex
|
||||||
@@ -472,10 +473,10 @@ def poll() {
|
|||||||
// GET THE LIST OF ACTIVITIES
|
// GET THE LIST OF ACTIVITIES
|
||||||
if (state.HarmonyAccessToken) {
|
if (state.HarmonyAccessToken) {
|
||||||
getActivityList()
|
getActivityList()
|
||||||
def Params = [auth: state.HarmonyAccessToken]
|
def Params = [auth: state.HarmonyAccessToken]
|
||||||
def url = "https://home.myharmony.com/cloudapi/state?${toQueryString(Params)}"
|
def url = "https://home.myharmony.com/cloudapi/state?${toQueryString(Params)}"
|
||||||
try {
|
try {
|
||||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||||
def map = [:]
|
def map = [:]
|
||||||
response.data.hubs.each {
|
response.data.hubs.each {
|
||||||
if (it.value.message == "OK") {
|
if (it.value.message == "OK") {
|
||||||
@@ -488,20 +489,20 @@ def poll() {
|
|||||||
def currentActivity = getActivityName(it.value.response.data.currentAvActivity,it.key)
|
def currentActivity = getActivityName(it.value.response.data.currentAvActivity,it.key)
|
||||||
hub.sendEvent(name: "currentActivity", value: currentActivity, descriptionText: "Current activity is ${currentActivity}", display: false)
|
hub.sendEvent(name: "currentActivity", value: currentActivity, descriptionText: "Current activity is ${currentActivity}", display: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.trace it.value.message
|
log.trace it.value.message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
def activities = getChildDevices()
|
def activities = getChildDevices()
|
||||||
def activitynotrunning = true
|
def activitynotrunning = true
|
||||||
activities.each { activity ->
|
activities.each { activity ->
|
||||||
def act = activity.deviceNetworkId.split('-')
|
def act = activity.deviceNetworkId.split('-')
|
||||||
if (act.size() > 2) {
|
if (act.size() > 2) {
|
||||||
def aux = map.find { it.key == act[1] }
|
def aux = map.find { it.key == act[1] }
|
||||||
if (aux) {
|
if (aux) {
|
||||||
def aux2 = aux.value.split(',')
|
def aux2 = aux.value.split(',')
|
||||||
def childDevice = getChildDevice(activity.deviceNetworkId)
|
def childDevice = getChildDevice(activity.deviceNetworkId)
|
||||||
if ((act[2] == aux2[0]) && (aux2[1] == "1" || aux2[1] == "2")) {
|
if ((act[2] == aux2[0]) && (aux2[1] == "1" || aux2[1] == "2")) {
|
||||||
childDevice?.sendEvent(name: "switch", value: "on")
|
childDevice?.sendEvent(name: "switch", value: "on")
|
||||||
if (aux2[1] == "1")
|
if (aux2[1] == "1")
|
||||||
@@ -511,30 +512,30 @@ def poll() {
|
|||||||
if (aux2[1] == "3")
|
if (aux2[1] == "3")
|
||||||
runIn(5, "poll", [overwrite: true])
|
runIn(5, "poll", [overwrite: true])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "Poll completed $map - $state.hubs"
|
return "Poll completed $map - $state.hubs"
|
||||||
}
|
}
|
||||||
} 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")
|
||||||
return "Harmony Access token has expired"
|
return "Harmony Access token has expired"
|
||||||
}
|
}
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
log.trace e
|
log.trace e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def getActivityList() {
|
def getActivityList() {
|
||||||
// GET ACTIVITY'S NAME
|
// 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)}"
|
||||||
try {
|
try {
|
||||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||||
response.data.hubs.each {
|
response.data.hubs.each {
|
||||||
def hub = getChildDevice("harmony-${it.key}")
|
def hub = getChildDevice("harmony-${it.key}")
|
||||||
if (hub) {
|
if (hub) {
|
||||||
@@ -547,10 +548,10 @@ def getActivityList() {
|
|||||||
}
|
}
|
||||||
activities += [id: "off", name: "Activity OFF", type: "0"]
|
activities += [id: "off", name: "Activity OFF", type: "0"]
|
||||||
log.trace activities
|
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
|
||||||
@@ -559,7 +560,7 @@ def getActivityList() {
|
|||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
log.trace e
|
log.trace e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return activity
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,16 +568,16 @@ def getActivityName(activity,hubId) {
|
|||||||
// GET ACTIVITY'S NAME
|
// GET ACTIVITY'S NAME
|
||||||
def actname = activity
|
def actname = activity
|
||||||
if (state.HarmonyAccessToken) {
|
if (state.HarmonyAccessToken) {
|
||||||
def Params = [auth: state.HarmonyAccessToken]
|
def Params = [auth: state.HarmonyAccessToken]
|
||||||
def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}"
|
def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}"
|
||||||
try {
|
try {
|
||||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||||
actname = response.data.data.activities[activity].name
|
actname = response.data.data.activities[activity].name
|
||||||
}
|
}
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
log.trace e
|
log.trace e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return actname
|
return actname
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,19 +585,19 @@ def getActivityId(activity,hubId) {
|
|||||||
// GET ACTIVITY'S NAME
|
// GET ACTIVITY'S NAME
|
||||||
def actid = activity
|
def actid = activity
|
||||||
if (state.HarmonyAccessToken) {
|
if (state.HarmonyAccessToken) {
|
||||||
def Params = [auth: state.HarmonyAccessToken]
|
def Params = [auth: state.HarmonyAccessToken]
|
||||||
def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}"
|
def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}"
|
||||||
try {
|
try {
|
||||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||||
response.data.data.activities.each {
|
response.data.data.activities.each {
|
||||||
if (it.value.name == activity)
|
if (it.value.name == activity)
|
||||||
actid = it.key
|
actid = it.key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
log.trace e
|
log.trace e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return actid
|
return actid
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -604,16 +605,16 @@ def getHubName(hubId) {
|
|||||||
// GET HUB'S NAME
|
// GET HUB'S NAME
|
||||||
def hubname = hubId
|
def hubname = hubId
|
||||||
if (state.HarmonyAccessToken) {
|
if (state.HarmonyAccessToken) {
|
||||||
def Params = [auth: state.HarmonyAccessToken]
|
def Params = [auth: state.HarmonyAccessToken]
|
||||||
def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/discover?${toQueryString(Params)}"
|
def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/discover?${toQueryString(Params)}"
|
||||||
try {
|
try {
|
||||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||||
hubname = response.data.data.name
|
hubname = response.data.data.name
|
||||||
}
|
}
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
log.trace e
|
log.trace e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hubname
|
return hubname
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,8 +625,8 @@ def sendNotification(msg) {
|
|||||||
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
|
||||||
|
|
||||||
def html = """{"code":200,"message":"OK"}"""
|
def html = """{"code":200,"message":"OK"}"""
|
||||||
render contentType: 'application/json', data: html
|
render contentType: 'application/json', data: html
|
||||||
@@ -659,12 +660,16 @@ def updateDevice() {
|
|||||||
} else {
|
} else {
|
||||||
def device = allDevices.find { it.id == params.id }
|
def device = allDevices.find { it.id == params.id }
|
||||||
if (device) {
|
if (device) {
|
||||||
if (arguments) {
|
if (device.hasCommand("$command")) {
|
||||||
device."$command"(*arguments)
|
if (arguments) {
|
||||||
} else {
|
device."$command"(*arguments)
|
||||||
device."$command"()
|
} else {
|
||||||
|
device."$command"()
|
||||||
|
}
|
||||||
|
render status: 204, data: "{}"
|
||||||
|
} else {
|
||||||
|
render status: 404, data: '{"msg": "Command not supported by this Device"}'
|
||||||
}
|
}
|
||||||
render status: 204, data: "{}"
|
|
||||||
} else {
|
} else {
|
||||||
render status: 404, data: '{"msg": "Device not found"}'
|
render status: 404, data: '{"msg": "Device not found"}'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ definition(
|
|||||||
description: "Allows you to control your Samsung TV from the SmartThings app. Perform basic functions like power Off, source, volume, channels and other remote control functions.",
|
description: "Allows you to control your Samsung TV from the SmartThings app. Perform basic functions like power Off, source, volume, channels and other remote control functions.",
|
||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%402x.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%402x.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%403x.png"
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%403x.png",
|
||||||
|
singleInstance: true
|
||||||
)
|
)
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
definition(
|
definition(
|
||||||
name: "Send HAM Bridge Command When…",
|
name: "Send HAM Bridge Command When",
|
||||||
namespace: "soletc.com",
|
namespace: "smartthings",
|
||||||
author: "Scottin Pollock",
|
author: "Scottin Pollock",
|
||||||
description: "Sends a command to your HAM Bridge server when SmartThings are activated.",
|
description: "Sends a command to your HAM Bridge server when SmartThings are activated.",
|
||||||
category: "Convenience",
|
category: "Convenience",
|
||||||
@@ -25,7 +25,6 @@ definition(
|
|||||||
iconX2Url: "http://solutionsetcetera.com/stuff/STIcons/HB@2x.png"
|
iconX2Url: "http://solutionsetcetera.com/stuff/STIcons/HB@2x.png"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
section("Choose one or more, when..."){
|
section("Choose one or more, when..."){
|
||||||
input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
|
input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
|
||||||
|
|||||||
@@ -10,24 +10,24 @@
|
|||||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
* 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.
|
* for the specific language governing permissions and limitations under the License.
|
||||||
*
|
*
|
||||||
* Sonos Control
|
* Speaker Control
|
||||||
*
|
*
|
||||||
* Author: SmartThings
|
* Author: SmartThings
|
||||||
*
|
*
|
||||||
* Date: 2013-12-10
|
* Date: 2013-12-10
|
||||||
*/
|
*/
|
||||||
definition(
|
definition(
|
||||||
name: "Sonos Control",
|
name: "Speaker Control",
|
||||||
namespace: "smartthings",
|
namespace: "smartthings",
|
||||||
author: "SmartThings",
|
author: "SmartThings",
|
||||||
description: "Play or pause your Sonos when certain actions take place in your home.",
|
description: "Play or pause your Speaker when certain actions take place in your home.",
|
||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
|
||||||
)
|
)
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
page(name: "mainPage", title: "Control your Sonos when something happens", install: true, uninstall: true)
|
page(name: "mainPage", title: "Control your Speaker when something happens", install: true, uninstall: true)
|
||||||
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
||||||
section {
|
section {
|
||||||
input "starting", "time", title: "Starting", required: false
|
input "starting", "time", title: "Starting", required: false
|
||||||
@@ -81,7 +81,7 @@ def mainPage() {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
input "sonos", "capability.musicPlayer", title: "Sonos music player", required: true
|
input "sonos", "capability.musicPlayer", title: "Speaker music player", required: true
|
||||||
}
|
}
|
||||||
section("More options", hideable: true, hidden: true) {
|
section("More options", hideable: true, hidden: true) {
|
||||||
input "volume", "number", title: "Set the volume volume", description: "0-100%", required: false
|
input "volume", "number", title: "Set the volume volume", description: "0-100%", required: false
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
* 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.
|
* for the specific language governing permissions and limitations under the License.
|
||||||
*
|
*
|
||||||
* Sonos Mood Music
|
* Speaker Mood Music
|
||||||
*
|
*
|
||||||
* Author: SmartThings
|
* Author: SmartThings
|
||||||
* Date: 2014-02-12
|
* Date: 2014-02-12
|
||||||
@@ -65,7 +65,7 @@ private saveSelectedSong() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
definition(
|
definition(
|
||||||
name: "Sonos Mood Music",
|
name: "Speaker Mood Music",
|
||||||
namespace: "smartthings",
|
namespace: "smartthings",
|
||||||
author: "SmartThings",
|
author: "SmartThings",
|
||||||
description: "Plays a selected song or station.",
|
description: "Plays a selected song or station.",
|
||||||
@@ -75,7 +75,7 @@ definition(
|
|||||||
)
|
)
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
page(name: "mainPage", title: "Play a selected song or station on your Sonos when something happens", nextPage: "chooseTrack", uninstall: true)
|
page(name: "mainPage", title: "Play a selected song or station on your Speaker when something happens", nextPage: "chooseTrack", uninstall: true)
|
||||||
page(name: "chooseTrack", title: "Select a song", install: true)
|
page(name: "chooseTrack", title: "Select a song", install: true)
|
||||||
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
||||||
section {
|
section {
|
||||||
@@ -125,7 +125,7 @@ def mainPage() {
|
|||||||
ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
|
ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true
|
input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
|
||||||
}
|
}
|
||||||
section("More options", hideable: true, hidden: true) {
|
section("More options", hideable: true, hidden: true) {
|
||||||
input "volume", "number", title: "Set the volume", description: "0-100%", required: false
|
input "volume", "number", title: "Set the volume", description: "0-100%", required: false
|
||||||
@@ -10,23 +10,23 @@
|
|||||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
* 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.
|
* for the specific language governing permissions and limitations under the License.
|
||||||
*
|
*
|
||||||
* Sonos Custom Message
|
* Speaker Custom Message
|
||||||
*
|
*
|
||||||
* Author: SmartThings
|
* Author: SmartThings
|
||||||
* Date: 2014-1-29
|
* Date: 2014-1-29
|
||||||
*/
|
*/
|
||||||
definition(
|
definition(
|
||||||
name: "Sonos Notify with Sound",
|
name: "Speaker Notify with Sound",
|
||||||
namespace: "smartthings",
|
namespace: "smartthings",
|
||||||
author: "SmartThings",
|
author: "SmartThings",
|
||||||
description: "Play a sound or custom message through your Sonos when the mode changes or other events occur.",
|
description: "Play a sound or custom message through your Speaker when the mode changes or other events occur.",
|
||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
|
||||||
)
|
)
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
page(name: "mainPage", title: "Play a message on your Sonos when something happens", install: true, uninstall: true)
|
page(name: "mainPage", title: "Play a message on your Speaker when something happens", install: true, uninstall: true)
|
||||||
page(name: "chooseTrack", title: "Select a song or station")
|
page(name: "chooseTrack", title: "Select a song or station")
|
||||||
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
||||||
section {
|
section {
|
||||||
@@ -92,7 +92,7 @@ def mainPage() {
|
|||||||
input "message","text",title:"Play this message", required:false, multiple: false
|
input "message","text",title:"Play this message", required:false, multiple: false
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true
|
input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
|
||||||
}
|
}
|
||||||
section("More options", hideable: true, hidden: true) {
|
section("More options", hideable: true, hidden: true) {
|
||||||
input "resumePlaying", "bool", title: "Resume currently playing music after notification", required: false, defaultValue: true
|
input "resumePlaying", "bool", title: "Resume currently playing music after notification", required: false, defaultValue: true
|
||||||
@@ -10,23 +10,23 @@
|
|||||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
* 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.
|
* for the specific language governing permissions and limitations under the License.
|
||||||
*
|
*
|
||||||
* Sonos Weather Forecast
|
* Speaker Weather Forecast
|
||||||
*
|
*
|
||||||
* Author: SmartThings
|
* Author: SmartThings
|
||||||
* Date: 2014-1-29
|
* Date: 2014-1-29
|
||||||
*/
|
*/
|
||||||
definition(
|
definition(
|
||||||
name: "Sonos Weather Forecast",
|
name: "Speaker Weather Forecast",
|
||||||
namespace: "smartthings",
|
namespace: "smartthings",
|
||||||
author: "SmartThings",
|
author: "SmartThings",
|
||||||
description: "Play a weather report through your Sonos when the mode changes or other events occur",
|
description: "Play a weather report through your Speaker when the mode changes or other events occur",
|
||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
|
||||||
)
|
)
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
page(name: "mainPage", title: "Play the weather report on your sonos", install: true, uninstall: true)
|
page(name: "mainPage", title: "Play the weather report on your speaker", install: true, uninstall: true)
|
||||||
page(name: "chooseTrack", title: "Select a song or station")
|
page(name: "chooseTrack", title: "Select a song or station")
|
||||||
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
||||||
section {
|
section {
|
||||||
@@ -85,7 +85,7 @@ def mainPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true
|
input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
|
||||||
}
|
}
|
||||||
section("More options", hideable: true, hidden: true) {
|
section("More options", hideable: true, hidden: true) {
|
||||||
input "resumePlaying", "bool", title: "Resume currently playing music after weather report finishes", required: false, defaultValue: true
|
input "resumePlaying", "bool", title: "Resume currently playing music after weather report finishes", required: false, defaultValue: true
|
||||||
@@ -23,7 +23,8 @@ definition(
|
|||||||
description: "Integrate your Tesla car with SmartThings.",
|
description: "Integrate your Tesla car with SmartThings.",
|
||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%402x.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%402x.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%403x.png"
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%403x.png",
|
||||||
|
singleInstance: true
|
||||||
)
|
)
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ definition(
|
|||||||
description: "Allows you to integrate your WeMo Switch and Wemo Motion sensor with SmartThings.",
|
description: "Allows you to integrate your WeMo Switch and Wemo Motion sensor with SmartThings.",
|
||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo@2x.png"
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo@2x.png",
|
||||||
|
singleInstance: true
|
||||||
)
|
)
|
||||||
|
|
||||||
preferences {
|
preferences {
|
||||||
@@ -61,10 +62,7 @@ def firstPage()
|
|||||||
|
|
||||||
log.debug "REFRESH COUNT :: ${refreshCount}"
|
log.debug "REFRESH COUNT :: ${refreshCount}"
|
||||||
|
|
||||||
if(!state.subscribe) {
|
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
|
||||||
state.subscribe = true
|
|
||||||
}
|
|
||||||
|
|
||||||
//ssdp request every 25 seconds
|
//ssdp request every 25 seconds
|
||||||
if((refreshCount % 5) == 0) {
|
if((refreshCount % 5) == 0) {
|
||||||
@@ -168,21 +166,30 @@ def getWemoLightSwitches()
|
|||||||
def installed() {
|
def installed() {
|
||||||
log.debug "Installed with settings: ${settings}"
|
log.debug "Installed with settings: ${settings}"
|
||||||
initialize()
|
initialize()
|
||||||
|
|
||||||
runIn(5, "subscribeToDevices") //initial subscriptions delayed by 5 seconds
|
|
||||||
runIn(10, "refreshDevices") //refresh devices, delayed by 10 seconds
|
|
||||||
runIn(900, "doDeviceSync" , [overwrite: false]) //setup ip:port syncing every 15 minutes
|
|
||||||
|
|
||||||
// SUBSCRIBE responses come back with TIMEOUT-1801 (30 minutes), so we refresh things a bit before they expire (29 minutes)
|
|
||||||
runIn(1740, "refresh", [overwrite: false])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def updated() {
|
def updated() {
|
||||||
log.debug "Updated with settings: ${settings}"
|
log.debug "Updated with settings: ${settings}"
|
||||||
initialize()
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
runIn(5, "subscribeToDevices") //subscribe again to new/old devices wait 5 seconds
|
def initialize() {
|
||||||
runIn(10, "refreshDevices") //refresh devices again, delayed by 10 seconds
|
unsubscribe()
|
||||||
|
unschedule()
|
||||||
|
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||||
|
|
||||||
|
if (selectedSwitches)
|
||||||
|
addSwitches()
|
||||||
|
|
||||||
|
if (selectedMotions)
|
||||||
|
addMotions()
|
||||||
|
|
||||||
|
if (selectedLightSwitches)
|
||||||
|
addLightSwitches()
|
||||||
|
|
||||||
|
runIn(5, "subscribeToDevices") //initial subscriptions delayed by 5 seconds
|
||||||
|
runIn(10, "refreshDevices") //refresh devices, delayed by 10 seconds
|
||||||
|
runEvery5Minutes("refresh")
|
||||||
}
|
}
|
||||||
|
|
||||||
def resubscribe() {
|
def resubscribe() {
|
||||||
@@ -192,8 +199,7 @@ def resubscribe() {
|
|||||||
|
|
||||||
def refresh() {
|
def refresh() {
|
||||||
log.debug "refresh() called"
|
log.debug "refresh() called"
|
||||||
//reschedule the refreshes
|
doDeviceSync()
|
||||||
runIn(1740, "refresh", [overwrite: false])
|
|
||||||
refreshDevices()
|
refreshDevices()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +242,8 @@ def addSwitches() {
|
|||||||
"port": selectedSwitch.value.port
|
"port": selectedSwitch.value.port
|
||||||
]
|
]
|
||||||
])
|
])
|
||||||
|
def ipvalue = convertHexToIP(selectedSwitch.value.ip)
|
||||||
|
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
|
||||||
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
|
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
|
||||||
} else {
|
} else {
|
||||||
log.debug "found ${d.displayName} with id $dni already exists"
|
log.debug "found ${d.displayName} with id $dni already exists"
|
||||||
@@ -266,8 +273,9 @@ def addMotions() {
|
|||||||
"port": selectedMotion.value.port
|
"port": selectedMotion.value.port
|
||||||
]
|
]
|
||||||
])
|
])
|
||||||
|
def ipvalue = convertHexToIP(selectedMotion.value.ip)
|
||||||
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
|
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
|
||||||
|
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
|
||||||
} else {
|
} else {
|
||||||
log.debug "found ${d.displayName} with id $dni already exists"
|
log.debug "found ${d.displayName} with id $dni already exists"
|
||||||
}
|
}
|
||||||
@@ -296,7 +304,8 @@ def addLightSwitches() {
|
|||||||
"port": selectedLightSwitch.value.port
|
"port": selectedLightSwitch.value.port
|
||||||
]
|
]
|
||||||
])
|
])
|
||||||
|
def ipvalue = convertHexToIP(selectedLightSwitch.value.ip)
|
||||||
|
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
|
||||||
log.debug "created ${d.displayName} with id $dni"
|
log.debug "created ${d.displayName} with id $dni"
|
||||||
} else {
|
} else {
|
||||||
log.debug "found ${d.displayName} with id $dni already exists"
|
log.debug "found ${d.displayName} with id $dni already exists"
|
||||||
@@ -304,27 +313,6 @@ def addLightSwitches() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def initialize() {
|
|
||||||
// remove location subscription afterwards
|
|
||||||
unsubscribe()
|
|
||||||
state.subscribe = false
|
|
||||||
|
|
||||||
if (selectedSwitches)
|
|
||||||
{
|
|
||||||
addSwitches()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedMotions)
|
|
||||||
{
|
|
||||||
addMotions()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedLightSwitches)
|
|
||||||
{
|
|
||||||
addLightSwitches()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def locationHandler(evt) {
|
def locationHandler(evt) {
|
||||||
def description = evt.description
|
def description = evt.description
|
||||||
def hub = evt?.hubId
|
def hub = evt?.hubId
|
||||||
@@ -333,53 +321,32 @@ def locationHandler(evt) {
|
|||||||
log.debug parsedEvent
|
log.debug parsedEvent
|
||||||
|
|
||||||
if (parsedEvent?.ssdpTerm?.contains("Belkin:device:controllee") || parsedEvent?.ssdpTerm?.contains("Belkin:device:insight")) {
|
if (parsedEvent?.ssdpTerm?.contains("Belkin:device:controllee") || parsedEvent?.ssdpTerm?.contains("Belkin:device:insight")) {
|
||||||
|
|
||||||
def switches = getWemoSwitches()
|
def switches = getWemoSwitches()
|
||||||
|
if (!(switches."${parsedEvent.ssdpUSN.toString()}")) {
|
||||||
if (!(switches."${parsedEvent.ssdpUSN.toString()}"))
|
//if it doesn't already exist
|
||||||
{ //if it doesn't already exist
|
|
||||||
switches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
|
switches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{ // just update the values
|
|
||||||
|
|
||||||
log.debug "Device was already found in state..."
|
log.debug "Device was already found in state..."
|
||||||
|
|
||||||
def d = switches."${parsedEvent.ssdpUSN.toString()}"
|
def d = switches."${parsedEvent.ssdpUSN.toString()}"
|
||||||
boolean deviceChangedValues = false
|
boolean deviceChangedValues = false
|
||||||
|
log.debug "$d.ip <==> $parsedEvent.ip"
|
||||||
if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
|
if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
|
||||||
d.ip = parsedEvent.ip
|
d.ip = parsedEvent.ip
|
||||||
d.port = parsedEvent.port
|
d.port = parsedEvent.port
|
||||||
deviceChangedValues = true
|
deviceChangedValues = true
|
||||||
log.debug "Device's port or ip changed..."
|
log.debug "Device's port or ip changed..."
|
||||||
|
def child = getChildDevice(parsedEvent.mac)
|
||||||
|
child.subscribe(parsedEvent.ip, parsedEvent.port)
|
||||||
|
child.poll()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deviceChangedValues) {
|
|
||||||
def children = getChildDevices()
|
|
||||||
log.debug "Found children ${children}"
|
|
||||||
children.each {
|
|
||||||
if (it.getDeviceDataByName("mac") == parsedEvent.mac) {
|
|
||||||
log.debug "updating ip and port, and resubscribing, for device ${it} with mac ${parsedEvent.mac}"
|
|
||||||
it.subscribe(parsedEvent.ip, parsedEvent.port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
else if (parsedEvent?.ssdpTerm?.contains("Belkin:device:sensor")) {
|
else if (parsedEvent?.ssdpTerm?.contains("Belkin:device:sensor")) {
|
||||||
|
|
||||||
def motions = getWemoMotions()
|
def motions = getWemoMotions()
|
||||||
|
if (!(motions."${parsedEvent.ssdpUSN.toString()}")) {
|
||||||
if (!(motions."${parsedEvent.ssdpUSN.toString()}"))
|
//if it doesn't already exist
|
||||||
{ //if it doesn't already exist
|
|
||||||
motions << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
|
motions << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
|
||||||
}
|
} else { // just update the values
|
||||||
else
|
|
||||||
{ // just update the values
|
|
||||||
|
|
||||||
log.debug "Device was already found in state..."
|
log.debug "Device was already found in state..."
|
||||||
|
|
||||||
def d = motions."${parsedEvent.ssdpUSN.toString()}"
|
def d = motions."${parsedEvent.ssdpUSN.toString()}"
|
||||||
@@ -412,10 +379,7 @@ def locationHandler(evt) {
|
|||||||
if (!(lightSwitches."${parsedEvent.ssdpUSN.toString()}"))
|
if (!(lightSwitches."${parsedEvent.ssdpUSN.toString()}"))
|
||||||
{ //if it doesn't already exist
|
{ //if it doesn't already exist
|
||||||
lightSwitches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
|
lightSwitches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{ // just update the values
|
|
||||||
|
|
||||||
log.debug "Device was already found in state..."
|
log.debug "Device was already found in state..."
|
||||||
|
|
||||||
def d = lightSwitches."${parsedEvent.ssdpUSN.toString()}"
|
def d = lightSwitches."${parsedEvent.ssdpUSN.toString()}"
|
||||||
@@ -426,21 +390,11 @@ def locationHandler(evt) {
|
|||||||
d.port = parsedEvent.port
|
d.port = parsedEvent.port
|
||||||
deviceChangedValues = true
|
deviceChangedValues = true
|
||||||
log.debug "Device's port or ip changed..."
|
log.debug "Device's port or ip changed..."
|
||||||
|
def child = getChildDevice(parsedEvent.mac)
|
||||||
|
log.debug "updating ip and port, and resubscribing, for device with mac ${parsedEvent.mac}"
|
||||||
|
child.subscribe(parsedEvent.ip, parsedEvent.port)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deviceChangedValues) {
|
|
||||||
def children = getChildDevices()
|
|
||||||
log.debug "Found children ${children}"
|
|
||||||
children.each {
|
|
||||||
if (it.getDeviceDataByName("mac") == parsedEvent.mac) {
|
|
||||||
log.debug "updating ip and port, and resubscribing, for device ${it} with mac ${parsedEvent.mac}"
|
|
||||||
it.subscribe(parsedEvent.ip, parsedEvent.port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
else if (parsedEvent.headers && parsedEvent.body) {
|
else if (parsedEvent.headers && parsedEvent.body) {
|
||||||
String headerString = new String(parsedEvent.headers.decodeBase64())?.toLowerCase()
|
String headerString = new String(parsedEvent.headers.decodeBase64())?.toLowerCase()
|
||||||
@@ -580,73 +534,30 @@ private def parseDiscoveryMessage(String description) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
device
|
device
|
||||||
}
|
}
|
||||||
|
|
||||||
def doDeviceSync(){
|
def doDeviceSync(){
|
||||||
log.debug "Doing Device Sync!"
|
log.debug "Doing Device Sync!"
|
||||||
runIn(900, "doDeviceSync" , [overwrite: false]) //schedule to run again in 15 minutes
|
|
||||||
|
|
||||||
if(!state.subscribe) {
|
|
||||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
|
||||||
state.subscribe = true
|
|
||||||
}
|
|
||||||
|
|
||||||
discoverAllWemoTypes()
|
discoverAllWemoTypes()
|
||||||
}
|
}
|
||||||
|
|
||||||
def pollChildren() {
|
private String convertHexToIP(hex) {
|
||||||
def devices = getAllChildDevices()
|
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||||
devices.each { d ->
|
|
||||||
//only poll switches?
|
|
||||||
d.poll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def delayPoll() {
|
private Integer convertHexToInt(hex) {
|
||||||
log.debug "Executing 'delayPoll'"
|
Integer.parseInt(hex,16)
|
||||||
|
|
||||||
runIn(5, "pollChildren")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*def poll() {
|
private Boolean canInstallLabs() {
|
||||||
log.debug "Executing 'poll'"
|
|
||||||
runIn(600, "poll", [overwrite: false]) //schedule to run again in 10 minutes
|
|
||||||
|
|
||||||
def lastPoll = getLastPollTime()
|
|
||||||
def currentTime = now()
|
|
||||||
def lastPollDiff = currentTime - lastPoll
|
|
||||||
log.debug "lastPoll: $lastPoll, currentTime: $currentTime, lastPollDiff: $lastPollDiff"
|
|
||||||
setLastPollTime(currentTime)
|
|
||||||
|
|
||||||
doDeviceSync()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def setLastPollTime(currentTime) {
|
|
||||||
state.lastpoll = currentTime
|
|
||||||
}
|
|
||||||
|
|
||||||
def getLastPollTime() {
|
|
||||||
state.lastpoll ?: now()
|
|
||||||
}
|
|
||||||
|
|
||||||
def now() {
|
|
||||||
new Date().getTime()
|
|
||||||
}*/
|
|
||||||
|
|
||||||
private Boolean canInstallLabs()
|
|
||||||
{
|
|
||||||
return hasAllHubsOver("000.011.00603")
|
return hasAllHubsOver("000.011.00603")
|
||||||
}
|
}
|
||||||
|
|
||||||
private Boolean hasAllHubsOver(String desiredFirmware)
|
private Boolean hasAllHubsOver(String desiredFirmware) {
|
||||||
{
|
|
||||||
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
|
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
|
||||||
}
|
}
|
||||||
|
|
||||||
private List getRealHubFirmwareVersions()
|
private List getRealHubFirmwareVersions() {
|
||||||
{
|
|
||||||
return location.hubs*.firmwareVersionString.findAll { it }
|
return location.hubs*.firmwareVersionString.findAll { it }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ definition(
|
|||||||
category: "Connections",
|
category: "Connections",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/withings.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/withings.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/withings%402x.png",
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/withings%402x.png",
|
||||||
oauth: true
|
oauth: true,
|
||||||
|
singleInstance: true
|
||||||
) {
|
) {
|
||||||
appSetting "clientId"
|
appSetting "clientId"
|
||||||
appSetting "clientSecret"
|
appSetting "clientSecret"
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ definition(
|
|||||||
category: "SmartThings Internal",
|
category: "SmartThings Internal",
|
||||||
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%402x.png",
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png",
|
||||||
oauth: true
|
oauth: true,
|
||||||
|
singleInstance: true
|
||||||
) {
|
) {
|
||||||
appSetting "serverUrl"
|
appSetting "serverUrl"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
definition(
|
definition(
|
||||||
name: "Sprayer Controller 2",
|
name: "Sprayer Controller 2",
|
||||||
namespace: "",
|
namespace: "sprayercontroller",
|
||||||
author: "Cooper Lee",
|
author: "Cooper Lee",
|
||||||
description: "Control Sprayers for a period of time a number of times per hour",
|
description: "Control Sprayers for a period of time a number of times per hour",
|
||||||
category: "My Apps",
|
category: "My Apps",
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ preferences {
|
|||||||
}
|
}
|
||||||
section("Send Notifications?") {
|
section("Send Notifications?") {
|
||||||
input("recipients", "contact", title: "Send notifications to") {
|
input("recipients", "contact", title: "Send notifications to") {
|
||||||
input "phone", "phone", title: "Send an SMS to this number?"
|
input "phone", "phone", title: "Send an SMS to this number?", required:false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +266,9 @@ def sendAway(msg) {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
sendPush(msg)
|
sendPush(msg)
|
||||||
sendSms(phone, msg)
|
if(phone){
|
||||||
|
sendSms(phone, msg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +282,9 @@ def sendHome(msg) {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
sendPush(msg)
|
sendPush(msg)
|
||||||
sendSms(phone, msg)
|
if(phone){
|
||||||
|
sendSms(phone, msg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,4 +343,4 @@ private getTimeIntervalLabel() {
|
|||||||
|
|
||||||
private hideOptionsSection() {
|
private hideOptionsSection() {
|
||||||
(starting || ending || days || modes) ? false: true
|
(starting || ending || days || modes) ? false: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ definition(
|
|||||||
description: "Connect your Quirky to SmartThings.",
|
description: "Connect your Quirky to SmartThings.",
|
||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky@2x.png"
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky@2x.png",
|
||||||
|
singleInstance: true
|
||||||
) {
|
) {
|
||||||
appSetting "clientId"
|
appSetting "clientId"
|
||||||
appSetting "clientSecret"
|
appSetting "clientSecret"
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ definition(
|
|||||||
description: "Connect your TCP bulbs to SmartThings using Cloud to Cloud integration. You must create a remote login acct on TCP Mobile App.",
|
description: "Connect your TCP bulbs to SmartThings using Cloud to Cloud integration. You must create a remote login acct on TCP Mobile App.",
|
||||||
category: "SmartThings Labs",
|
category: "SmartThings Labs",
|
||||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp.png",
|
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp.png",
|
||||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp@2x.png"
|
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp@2x.png",
|
||||||
|
singleInstance: true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user