mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-09 13:21:53 +00:00
Compare commits
106 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
625940423b | ||
|
|
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 | ||
|
|
794ff6b68a | ||
|
|
ef21fd4257 | ||
|
|
1965f10584 | ||
|
|
9c9fba0939 | ||
|
|
725f9ebec7 | ||
|
|
e217805d98 | ||
|
|
ff39270ba4 | ||
|
|
d2ece83b47 | ||
|
|
66dbc02274 | ||
|
|
5f899a48d0 | ||
|
|
72899ee036 | ||
|
|
9ff67e9e17 | ||
|
|
f6791d1744 | ||
|
|
8f31b48974 | ||
|
|
b6dd5168d1 | ||
|
|
8a37d5715a | ||
|
|
4ef1d12c61 | ||
|
|
045a024bca | ||
|
|
4d5bf094aa | ||
|
|
9d016839c8 | ||
|
|
e4ce916d8f | ||
|
|
d6668a1e86 | ||
|
|
ecb975540b | ||
|
|
cd81871f90 | ||
|
|
73415f59e1 | ||
|
|
30fdb92141 | ||
|
|
587b3295ae | ||
|
|
9538df65e5 | ||
|
|
6854665f68 | ||
|
|
2534afbf81 | ||
|
|
eb3d0c2874 | ||
|
|
5f85cd2873 | ||
|
|
7bb6f67dbc | ||
|
|
05cf0a0cb1 | ||
|
|
f012419710 | ||
|
|
239f771ac1 | ||
|
|
87b6715a00 | ||
|
|
d6a96317bf | ||
|
|
6d64212c93 | ||
|
|
088e746f99 | ||
|
|
c26701383e | ||
|
|
0b8f1d0168 | ||
|
|
b78337c96b | ||
|
|
9fcd327da2 | ||
|
|
c3ce69994e | ||
|
|
5bd03d1914 | ||
|
|
950780d30c | ||
|
|
8040ddd6f7 | ||
|
|
4863b2345e | ||
|
|
32b4914ba0 | ||
|
|
c76a2e807b | ||
|
|
39e7ddb781 | ||
|
|
45f08df026 | ||
|
|
09b91014da | ||
|
|
5edff0df53 | ||
|
|
617d53da43 | ||
|
|
8ba5eaf74d |
@@ -0,0 +1,513 @@
|
||||
// keen home smart vent
|
||||
// http://www.keenhome.io
|
||||
// SmartThings Device Handler v1.0.0
|
||||
|
||||
metadata {
|
||||
definition (name: "Keen Home Smart Vent", namespace: "Keen Home", author: "Keen Home") {
|
||||
capability "Switch Level"
|
||||
capability "Switch"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Temperature Measurement"
|
||||
capability "Battery"
|
||||
|
||||
command "getLevel"
|
||||
command "getOnOff"
|
||||
command "getPressure"
|
||||
command "getBattery"
|
||||
command "getTemperature"
|
||||
command "setZigBeeIdTile"
|
||||
command "clearObstruction"
|
||||
|
||||
fingerprint endpoint: "1",
|
||||
profileId: "0104",
|
||||
inClusters: "0000,0001,0003,0004,0005,0006,0008,0020,0402,0403,0B05,FC01,FC02",
|
||||
outClusters: "0019"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
// status messages
|
||||
status "on": "on/off: 1"
|
||||
status "off": "on/off: 0"
|
||||
|
||||
// reply messages
|
||||
reply "zcl on-off on": "on/off: 1"
|
||||
reply "zcl on-off off": "on/off: 0"
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles {
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
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 "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) {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
valueTile("temperature", "device.temperature", inactiveLabel: false) {
|
||||
state "temperature", label:'${currentValue}°',
|
||||
backgroundColors:[
|
||||
[value: 31, color: "#153591"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
}
|
||||
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") {
|
||||
state "battery", label: 'Battery \n${currentValue}%', backgroundColor:"#ffffff"
|
||||
}
|
||||
valueTile("zigbeeId", "device.zigbeeId", inactiveLabel: true, decoration: "flat") {
|
||||
state "serial", label:'${currentValue}', backgroundColor:"#ffffff"
|
||||
}
|
||||
main "switch"
|
||||
details(["switch","refresh","temperature","levelSliderControl","battery"])
|
||||
}
|
||||
}
|
||||
|
||||
/**** PARSE METHODS ****/
|
||||
def parse(String description) {
|
||||
log.debug "description: $description"
|
||||
|
||||
Map map = [:]
|
||||
if (description?.startsWith('catchall:')) {
|
||||
map = parseCatchAllMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('read attr -')) {
|
||||
map = parseReportAttributeMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('temperature: ') || description?.startsWith('humidity: ')) {
|
||||
map = parseCustomMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('on/off: ')) {
|
||||
map = parseOnOffMessage(description)
|
||||
}
|
||||
|
||||
log.debug "Parse returned $map"
|
||||
return map ? createEvent(map) : null
|
||||
}
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
log.debug "parseCatchAllMessage"
|
||||
|
||||
def cluster = zigbee.parse(description)
|
||||
log.debug "cluster: ${cluster}"
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
log.debug "processing message"
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0001:
|
||||
return makeBatteryResult(cluster.data.last())
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = convertTemperatureHex(temp)
|
||||
return makeTemperatureResult(value)
|
||||
break
|
||||
|
||||
case 0x0006:
|
||||
return makeOnOffResult(cluster.data[-1])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return [:]
|
||||
}
|
||||
|
||||
private boolean shouldProcessMessage(cluster) {
|
||||
// 0x0B is default response indicating message got through
|
||||
// 0x07 is bind message
|
||||
if (cluster.profileId != 0x0104 ||
|
||||
cluster.command == 0x0B ||
|
||||
cluster.command == 0x07 ||
|
||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private Map parseReportAttributeMessage(String description) {
|
||||
log.debug "parseReportAttributeMessage"
|
||||
|
||||
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
log.debug "Desc Map: $descMap"
|
||||
|
||||
if (descMap.cluster == "0006" && descMap.attrId == "0000") {
|
||||
return makeOnOffResult(Int.parseInt(descMap.value));
|
||||
}
|
||||
else if (descMap.cluster == "0008" && descMap.attrId == "0000") {
|
||||
return makeLevelResult(descMap.value)
|
||||
}
|
||||
else if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
||||
def value = convertTemperatureHex(descMap.value)
|
||||
return makeTemperatureResult(value)
|
||||
}
|
||||
else if (descMap.cluster == "0001" && descMap.attrId == "0021") {
|
||||
return makeBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||
}
|
||||
else if (descMap.cluster == "0403" && descMap.attrId == "0020") {
|
||||
return makePressureResult(Integer.parseInt(descMap.value, 16))
|
||||
}
|
||||
else if (descMap.cluster == "0000" && descMap.attrId == "0006") {
|
||||
return makeSerialResult(new String(descMap.value.decodeHex()))
|
||||
}
|
||||
|
||||
// shouldn't get here
|
||||
return [:]
|
||||
}
|
||||
|
||||
private Map parseCustomMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
if (description?.startsWith('temperature: ')) {
|
||||
// log.debug "${description}"
|
||||
// def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale())
|
||||
// log.debug "split: " + description.split(": ")
|
||||
def value = Double.parseDouble(description.split(": ")[1])
|
||||
// log.debug "${value}"
|
||||
resultMap = makeTemperatureResult(convertTemperature(value))
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private Map parseOnOffMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
if (description?.startsWith('on/off: ')) {
|
||||
def value = Integer.parseInt(description - "on/off: ")
|
||||
resultMap = makeOnOffResult(value)
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private Map makeOnOffResult(rawValue) {
|
||||
log.debug "makeOnOffResult: ${rawValue}"
|
||||
def linkText = getLinkText(device)
|
||||
def value = rawValue == 1 ? "on" : "off"
|
||||
return [
|
||||
name: "switch",
|
||||
value: value,
|
||||
descriptionText: "${linkText} is ${value}"
|
||||
]
|
||||
}
|
||||
|
||||
private Map makeLevelResult(rawValue) {
|
||||
def linkText = getLinkText(device)
|
||||
def value = Integer.parseInt(rawValue, 16)
|
||||
def rangeMax = 254
|
||||
|
||||
// catch obstruction level
|
||||
if (value == 255) {
|
||||
log.debug "${linkText} is obstructed"
|
||||
// Just return here. Once the vent is power cycled
|
||||
// it will go back to the previous level before obstruction.
|
||||
// Therefore, no need to update level on the display.
|
||||
return [
|
||||
name: "switch",
|
||||
value: "obstructed",
|
||||
descriptionText: "${linkText} is obstructed. Please power cycle."
|
||||
]
|
||||
}
|
||||
|
||||
value = Math.floor(value / rangeMax * 100)
|
||||
|
||||
return [
|
||||
name: "level",
|
||||
value: value,
|
||||
descriptionText: "${linkText} level is ${value}%"
|
||||
]
|
||||
}
|
||||
|
||||
private Map makePressureResult(rawValue) {
|
||||
log.debug 'makePressureResut'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
def pascals = rawValue / 10
|
||||
def result = [
|
||||
name: 'pressure',
|
||||
descriptionText: "${linkText} pressure is ${pascals}Pa",
|
||||
value: pascals
|
||||
]
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private Map makeBatteryResult(rawValue) {
|
||||
// log.debug 'makeBatteryResult'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
// log.debug
|
||||
[
|
||||
name: 'battery',
|
||||
value: rawValue,
|
||||
descriptionText: "${linkText} battery is at ${rawValue}%"
|
||||
]
|
||||
}
|
||||
|
||||
private Map makeTemperatureResult(value) {
|
||||
// log.debug 'makeTemperatureResult'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
// log.debug "tempOffset: ${tempOffset}"
|
||||
if (tempOffset) {
|
||||
def offset = tempOffset as int
|
||||
// log.debug "offset: ${offset}"
|
||||
def v = value as int
|
||||
// log.debug "v: ${v}"
|
||||
value = v + offset
|
||||
// log.debug "value: ${value}"
|
||||
}
|
||||
|
||||
return [
|
||||
name: 'temperature',
|
||||
value: "" + value,
|
||||
descriptionText: "${linkText} is ${value}°${temperatureScale}",
|
||||
]
|
||||
}
|
||||
|
||||
/**** HELPER METHODS ****/
|
||||
private def convertTemperatureHex(value) {
|
||||
// log.debug "convertTemperatureHex(${value})"
|
||||
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
||||
// log.debug "celsius: ${celsius}"
|
||||
|
||||
return convertTemperature(celsius)
|
||||
}
|
||||
|
||||
private def convertTemperature(celsius) {
|
||||
// log.debug "convertTemperature()"
|
||||
|
||||
if(getTemperatureScale() == "C"){
|
||||
return celsius
|
||||
} else {
|
||||
def fahrenheit = Math.round(celsiusToFahrenheit(celsius) * 100) /100
|
||||
// log.debug "converted to F: ${fahrenheit}"
|
||||
return fahrenheit
|
||||
}
|
||||
}
|
||||
|
||||
private def makeSerialResult(serial) {
|
||||
log.debug "makeSerialResult: " + serial
|
||||
|
||||
def linkText = getLinkText(device)
|
||||
sendEvent([
|
||||
name: "serial",
|
||||
value: serial,
|
||||
descriptionText: "${linkText} has serial ${serial}" ])
|
||||
return [
|
||||
name: "serial",
|
||||
value: serial,
|
||||
descriptionText: "${linkText} has serial ${serial}" ]
|
||||
}
|
||||
|
||||
// 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 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))
|
||||
"st cmd 0x${device.deviceNetworkId} 1 6 1 {}"
|
||||
}
|
||||
|
||||
def 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))
|
||||
"st cmd 0x${device.deviceNetworkId} 1 6 0 {}"
|
||||
}
|
||||
|
||||
def clearObstruction() {
|
||||
def linkText = getLinkText(device)
|
||||
log.debug "attempting to clear ${linkText} obstruction"
|
||||
|
||||
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) {
|
||||
log.debug "setting level: ${value}"
|
||||
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)
|
||||
if (value > 0) {
|
||||
sendEvent(name: "switch", value: "on", descriptionText: "${linkText} is on by setting a level")
|
||||
}
|
||||
else {
|
||||
sendEvent(name: "switch", value: "off", descriptionText: "${linkText} is off by setting level to 0")
|
||||
}
|
||||
|
||||
makeLevelCommand(value)
|
||||
}
|
||||
|
||||
def 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"]
|
||||
}
|
||||
|
||||
def getPressure() {
|
||||
log.debug "getPressure()"
|
||||
|
||||
// using a Keen Home specific attribute in the pressure measurement cluster
|
||||
[
|
||||
"zcl mfg-code 0x115B", "delay 200",
|
||||
"zcl global read 0x0403 0x20", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
|
||||
]
|
||||
}
|
||||
|
||||
def getLevel() {
|
||||
log.debug "getLevel()"
|
||||
|
||||
// disallow level updates while vent is obstructed
|
||||
if (device.currentValue("switch") == "obstructed") {
|
||||
log.error("cannot update level status because ${getLinkText(device)} is obstructed")
|
||||
return []
|
||||
}
|
||||
|
||||
["st rattr 0x${device.deviceNetworkId} 1 0x0008 0x0000"]
|
||||
}
|
||||
|
||||
def getTemperature() {
|
||||
log.debug "getTemperature()"
|
||||
|
||||
["st rattr 0x${device.deviceNetworkId} 1 0x0402 0"]
|
||||
}
|
||||
|
||||
def getBattery() {
|
||||
log.debug "getBattery()"
|
||||
|
||||
["st rattr 0x${device.deviceNetworkId} 1 0x0001 0x0021"]
|
||||
}
|
||||
|
||||
def setZigBeeIdTile() {
|
||||
log.debug "setZigBeeIdTile() - ${device.zigbeeId}"
|
||||
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
sendEvent([
|
||||
name: "zigbeeId",
|
||||
value: device.zigbeeId,
|
||||
descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ])
|
||||
return [
|
||||
name: "zigbeeId",
|
||||
value: device.zigbeeId,
|
||||
descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
getOnOff() +
|
||||
getLevel() +
|
||||
getTemperature() +
|
||||
getPressure() +
|
||||
getBattery()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "CONFIGURE"
|
||||
|
||||
// get ZigBee ID by hidden tile because that's the only way we can do it
|
||||
setZigBeeIdTile()
|
||||
|
||||
def configCmds = [
|
||||
// bind reporting clusters to hub
|
||||
"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 0x0402 {${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"
|
||||
|
||||
// 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
|
||||
// "zcl global send-me-a-report 6 0 0x10 5 60 {01}", "delay 200",
|
||||
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
// report with these parameters is preconfigured in firmware, can be overridden here
|
||||
// vent level - type: int8u, change: 1
|
||||
// "zcl global send-me-a-report 8 0 0x20 5 60 {01}", "delay 200",
|
||||
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
// report with these parameters is preconfigured in firmware, can be overridden here
|
||||
// temperature - type: int16s, change: 0xA = 10 = 0.1C
|
||||
// "zcl global send-me-a-report 0x0402 0 0x29 60 60 {0A00}", "delay 200",
|
||||
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
// report with these parameters is preconfigured in firmware, can be overridden here
|
||||
// keen home custom pressure (tenths of Pascals) - type: int32u, change: 1 = 0.1Pa
|
||||
// "zcl mfg-code 0x115B", "delay 200",
|
||||
// "zcl global send-me-a-report 0x0403 0x20 0x22 60 60 {010000}", "delay 200",
|
||||
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
// report with these parameters is preconfigured in firmware, can be overridden here
|
||||
// battery - type: int8u, change: 1
|
||||
// "zcl global send-me-a-report 1 0x21 0x20 60 3600 {01}", "delay 200",
|
||||
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
]
|
||||
|
||||
return configCmds + refresh()
|
||||
}
|
||||
@@ -24,6 +24,8 @@ metadata {
|
||||
capability "Battery"
|
||||
|
||||
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"
|
||||
}
|
||||
@@ -63,6 +65,19 @@ metadata {
|
||||
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) {
|
||||
multiAttributeTile(name:"motion", type: "generic", width: 6, height: 4){
|
||||
tileAttribute ("device.motion", key: "PRIMARY_CONTROL") {
|
||||
@@ -85,53 +100,78 @@ metadata {
|
||||
valueTile("humidity", "device.humidity", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "humidity", label:'${currentValue}% humidity', unit:""
|
||||
}
|
||||
|
||||
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) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
|
||||
main(["motion", "temperature", "humidity", "illuminance"])
|
||||
details(["motion", "temperature", "humidity", "illuminance", "battery"])
|
||||
valueTile("batteryStatus", "device.batteryStatus", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
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()
|
||||
{
|
||||
if (state.sec && !isConfigured()) {
|
||||
// in case we miss the SCSR
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
log.debug "${device.displayName} is now ${device.latestValue("powerSupply")}"
|
||||
|
||||
if (device.latestValue("powerSupply") == "USB Cable") { //case1: USB powered
|
||||
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
|
||||
if (description.startsWith("Err 106")) {
|
||||
state.sec = 0
|
||||
log.debug "parse() >> Err 106"
|
||||
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") {
|
||||
log.debug "parse() >> zwave.parse(description)"
|
||||
def cmd = zwave.parse(description, [0x31: 5, 0x30: 2, 0x84: 1])
|
||||
if (cmd) {
|
||||
result = zwaveEvent(cmd)
|
||||
}
|
||||
}
|
||||
log.debug "Parsed '${description}' to ${result.inspect()}"
|
||||
log.debug "After zwaveEvent(cmd) >> Parsed '${description}' to ${result.inspect()}"
|
||||
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 cmds = []
|
||||
if (!isConfigured()) {
|
||||
// we're still in the process of configuring a newly joined device
|
||||
log.debug("late configure")
|
||||
result += response(configure())
|
||||
result << response(configure())
|
||||
} else {
|
||||
result += response(zwave.wakeUpV1.wakeUpNoMoreInformation())
|
||||
log.debug("Device has been configured sending >> wakeUpNoMoreInformation()")
|
||||
cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format()
|
||||
result << response(cmds)
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -149,10 +189,29 @@ def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulat
|
||||
}
|
||||
|
||||
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 result = []
|
||||
def map = [ name: "battery", unit: "%" ]
|
||||
if (cmd.batteryLevel == 0xFF) {
|
||||
map.value = 1
|
||||
@@ -162,11 +221,14 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||
map.value = cmd.batteryLevel
|
||||
}
|
||||
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 = [:]
|
||||
switch (cmd.sensorType) {
|
||||
case 1:
|
||||
@@ -208,7 +270,6 @@ def motionEvent(value) {
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) {
|
||||
setConfigured()
|
||||
motionEvent(cmd.sensorValue)
|
||||
}
|
||||
|
||||
@@ -225,47 +286,112 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm
|
||||
result << createEvent(name: "tamper", value: "clear", displayed: false)
|
||||
break
|
||||
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
|
||||
case 7:
|
||||
result << motionEvent(1)
|
||||
break
|
||||
}
|
||||
} else {
|
||||
log.warn "Need to handle this cmd.notificationType: ${cmd.notificationType}"
|
||||
result << createEvent(descriptionText: cmd.toString(), isStateChange: false)
|
||||
}
|
||||
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) {
|
||||
log.debug "General zwaveEvent cmd: ${cmd}"
|
||||
createEvent(descriptionText: cmd.toString(), isStateChange: false)
|
||||
}
|
||||
|
||||
def configure() {
|
||||
// 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 "Multi 6 not sending configure until secure"
|
||||
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),
|
||||
log.debug "${device.displayName} is configuring its settings"
|
||||
def request = []
|
||||
|
||||
// report every 8 minutes (threshold reports don't work on battery power)
|
||||
zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 8*60),
|
||||
//1. set association groups for hub
|
||||
request << zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)
|
||||
|
||||
// report automatically on threshold change
|
||||
zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 1),
|
||||
request << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId)
|
||||
|
||||
//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()]
|
||||
}
|
||||
|
||||
private setConfigured() {
|
||||
updateDataValue("configured", "true")
|
||||
private def getTimeOptionValueMap() { [
|
||||
"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() {
|
||||
@@ -281,5 +407,6 @@ private command(physicalgraph.zwave.Command cmd) {
|
||||
}
|
||||
|
||||
private commands(commands, delay=200) {
|
||||
log.info "sending commands: ${commands}"
|
||||
delayBetween(commands.collect{ command(it) }, delay)
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ metadata {
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival2.png"
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival1.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival2.jpg"
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ metadata {
|
||||
tiles {
|
||||
standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) {
|
||||
state "present", labelIcon:"st.presence.tile.present", backgroundColor:"#53a7c0"
|
||||
state "not present", labelIcon:"st.presence.tile.not-present", backgroundColor:"#ffffff"
|
||||
state "not present", labelIcon:"st.presence.tile.not-present", backgroundColor:"#ebeef2"
|
||||
}
|
||||
standardTile("beep", "device.beep", decoration: "flat") {
|
||||
state "beep", label:'', action:"tone.beep", icon:"st.secondary.beep", backgroundColor:"#ffffff"
|
||||
|
||||
@@ -38,168 +38,63 @@ metadata {
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles {
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff"
|
||||
state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||
state "level", label: 'Level ${currentValue}%'
|
||||
}
|
||||
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "level", "levelSliderControl", "refresh"])
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
main "switch"
|
||||
details(["switch", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
|
||||
def parse(String description) {
|
||||
log.trace description
|
||||
if (description?.startsWith("catchall:")) {
|
||||
def msg = zigbee.parse(description)
|
||||
log.trace msg
|
||||
log.trace "data: $msg.data"
|
||||
|
||||
if(description?.endsWith("0100") ||description?.endsWith("1001"))
|
||||
{
|
||||
def result = createEvent(name: "switch", value: "on")
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
log.debug "description is $description"
|
||||
|
||||
def resultMap = zigbee.getKnownDescription(description)
|
||||
if (resultMap) {
|
||||
log.info resultMap
|
||||
if (resultMap.type == "update") {
|
||||
log.info "$device updates: ${resultMap.value}"
|
||||
}
|
||||
|
||||
if(description?.endsWith("0000") || description?.endsWith("1000"))
|
||||
{
|
||||
def result = createEvent(name: "switch", value: "off")
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
else {
|
||||
sendEvent(name: resultMap.type, value: resultMap.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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() {
|
||||
log.debug "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"
|
||||
]
|
||||
zigbee.on()
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
log.trace "setLevel($value)"
|
||||
def cmds = []
|
||||
zigbee.setLevel(value)
|
||||
}
|
||||
|
||||
if (value == 0) {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
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 refresh() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
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
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
}
|
||||
|
||||
@@ -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 on", action:"indicator.indicatorNever", icon:"st.indicators.lit-when-on"
|
||||
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"])
|
||||
details(["switch", "refresh", "indicator"])
|
||||
details(["switch", "level", "indicator", "refresh"])
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
def item1 = [
|
||||
canBeCurrentState: false,
|
||||
linkText: getLinkText(device),
|
||||
isStateChange: false,
|
||||
displayed: false,
|
||||
descriptionText: description,
|
||||
value: description
|
||||
]
|
||||
def result
|
||||
def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 1])
|
||||
if (cmd) {
|
||||
result = createEvent(cmd, item1)
|
||||
def result = null
|
||||
if (description != "updated") {
|
||||
log.debug "parse() >> zwave.parse($description)"
|
||||
def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 1])
|
||||
if (cmd) {
|
||||
result = zwaveEvent(cmd)
|
||||
}
|
||||
}
|
||||
else {
|
||||
item1.displayed = displayed(description, item1.isStateChange)
|
||||
result = [item1]
|
||||
if (result?.name == 'hail' && hubFirmwareLessThan("000.011.00602")) {
|
||||
result = [result, response(zwave.basicV1.basicGet())]
|
||||
log.debug "Was hailed: requesting state update"
|
||||
} else {
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
}
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
result
|
||||
return result
|
||||
}
|
||||
|
||||
def createEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, Map item1) {
|
||||
def result = doCreateEvent(cmd, item1)
|
||||
for (int i = 0; i < result.size(); i++) {
|
||||
result[i].type = "physical"
|
||||
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
|
||||
dimmerEvents(cmd)
|
||||
}
|
||||
|
||||
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) {
|
||||
log.debug "ConfigurationReport $cmd"
|
||||
def value = "when off"
|
||||
if (cmd.configurationValue[0] == 1) {value = "when on"}
|
||||
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) {
|
||||
// Handles any Z-Wave commands we aren't interested in
|
||||
log.debug "UNHANDLED COMMAND $cmd"
|
||||
def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) {
|
||||
createEvent([name: "hail", value: "hail", descriptionText: "Switch button was pressed", displayed: false])
|
||||
}
|
||||
|
||||
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() {
|
||||
log.info "on"
|
||||
delayBetween([zwave.basicV1.basicSet(value: 0xFF).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
|
||||
delayBetween([
|
||||
zwave.basicV1.basicSet(value: 0xFF).format(),
|
||||
zwave.switchMultilevelV1.switchMultilevelGet().format()
|
||||
],5000)
|
||||
}
|
||||
|
||||
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) {
|
||||
log.debug "setLevel >> value: $value"
|
||||
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)
|
||||
}
|
||||
|
||||
def setLevel(value, duration) {
|
||||
log.debug "setLevel >> value: $value, duration: $duration"
|
||||
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)
|
||||
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() {
|
||||
@@ -197,21 +192,27 @@ def poll() {
|
||||
}
|
||||
|
||||
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() {
|
||||
sendEvent(name: "indicatorStatus", value: "when on", display: false)
|
||||
sendEvent(name: "indicatorStatus", value: "when on")
|
||||
zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 3, size: 1).format()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
def indicatorNever() {
|
||||
sendEvent(name: "indicatorStatus", value: "never", display: false)
|
||||
sendEvent(name: "indicatorStatus", value: "never")
|
||||
zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 3, size: 1).format()
|
||||
}
|
||||
|
||||
@@ -222,4 +223,4 @@ def invertSwitch(invert=true) {
|
||||
else {
|
||||
zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 4, size: 1).format()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Fibaro Smoke Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Battery" //attributes: battery
|
||||
capability "Configuration" //commands: configure()
|
||||
capability "Sensor"
|
||||
capability "Smoke Detector" //attributes: smoke ("detected","clear","tested")
|
||||
capability "Temperature Measurement" //attributes: temperature
|
||||
attribute "tamper", "enum", ["detected", "clear"]
|
||||
attribute "heatAlarm", "enum", ["overheat detected", "clear", "rapid temperature rise", "underheat detected"]
|
||||
fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x86, 0x72, 0x5A, 0x59, 0x85, 0x73, 0x84, 0x80, 0x71, 0x56, 0x70, 0x31, 0x8E, 0x22, 0x9C, 0x98, 0x7A", outClusters: "0x20, 0x8B"
|
||||
}
|
||||
simulator {
|
||||
//battery
|
||||
for (int i in [0, 5, 10, 15, 50, 99, 100]) {
|
||||
status "battery ${i}%": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate(
|
||||
new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: i)
|
||||
).incomingMessage()
|
||||
}
|
||||
status "battery 100%": "command: 8003, payload: 64"
|
||||
status "battery 5%": "command: 8003, payload: 05"
|
||||
//smoke
|
||||
status "smoke detected": "command: 7105, payload: 01 01"
|
||||
status "smoke clear": "command: 7105, payload: 01 00"
|
||||
status "smoke tested": "command: 7105, payload: 01 03"
|
||||
//temperature
|
||||
for (int i = 0; i <= 100; i += 20) {
|
||||
status "temperature ${i}F": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate(
|
||||
new physicalgraph.zwave.Zwave().sensorMultilevelV5.sensorMultilevelReport(scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1)
|
||||
).incomingMessage()
|
||||
}
|
||||
}
|
||||
preferences {
|
||||
input description: "After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration",
|
||||
title: "Instructions", displayDuringSetup: true, type: "paragraph", element: "paragraph"
|
||||
input description: "Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN",
|
||||
title: "To check smoke detection state", displayDuringSetup: true, type: "paragraph", element: "paragraph"
|
||||
input description: "Please consult Fibaro Smoke Sensor 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 "smokeSensorSensitivity", "enum", title: "Smoke Sensor Sensitivity", options: ["High","Medium","Low"], defaultValue: "${smokeSensorSensitivity}", displayDuringSetup: true
|
||||
input "zwaveNotificationStatus", "enum", title: "Notifications Status", options: ["disabled","casing opened","exceeding temperature threshold", "lack of Z-Wave range", "all notifications"],
|
||||
defaultValue: "${zwaveNotificationStatus}", displayDuringSetup: true
|
||||
input "visualIndicatorNotificationStatus", "enum", title: "Visual Indicator Notifications Status",
|
||||
options: ["disabled","casing opened","exceeding temperature threshold", "lack of Z-Wave range", "all notifications"],
|
||||
defaultValue: "${visualIndicatorNotificationStatus}", displayDuringSetup: true
|
||||
input "soundNotificationStatus", "enum", title: "Sound Notifications Status",
|
||||
options: ["disabled","casing opened","exceeding temperature threshold", "lack of Z-Wave range", "all notifications"],
|
||||
defaultValue: "${soundNotificationStatus}", displayDuringSetup: true
|
||||
input "temperatureReportInterval", "enum", title: "Temperature Report Interval",
|
||||
options: ["reports inactive", "5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${temperatureReportInterval}", displayDuringSetup: true
|
||||
input "temperatureReportHysteresis", "number", title: "Temperature Report Hysteresis", description: "Available settings: 1-100 C", range: "1..100", displayDuringSetup: true
|
||||
input "temperatureThreshold", "number", title: "Overheat Temperature Threshold", description: "Available settings: 0 or 2-100 C", range: "0..100", displayDuringSetup: true
|
||||
input "excessTemperatureSignalingInterval", "enum", title: "Excess Temperature Signaling Interval",
|
||||
options: ["5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${excessTemperatureSignalingInterval}", displayDuringSetup: true
|
||||
input "lackOfZwaveRangeIndicationInterval", "enum", title: "Lack of Z-Wave Range Indication Interval",
|
||||
options: ["5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${lackOfZwaveRangeIndicationInterval}", displayDuringSetup: true
|
||||
}
|
||||
tiles (scale: 2){
|
||||
multiAttributeTile(name:"smoke", type: "lighting", width: 6, height: 4){
|
||||
tileAttribute ("device.smoke", key: "PRIMARY_CONTROL") {
|
||||
attributeState("clear", label:"CLEAR", icon:"st.alarm.smoke.clear", backgroundColor:"#ffffff")
|
||||
attributeState("detected", label:"SMOKE", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13")
|
||||
attributeState("tested", label:"TEST", icon:"st.alarm.smoke.test", backgroundColor:"#e86d13")
|
||||
attributeState("replacement required", label:"REPLACE", icon:"st.alarm.smoke.test", backgroundColor:"#FFFF66")
|
||||
attributeState("unknown", label:"UNKNOWN", icon:"st.alarm.smoke.test", backgroundColor:"#ffffff")
|
||||
}
|
||||
tileAttribute ("device.battery", key: "SECONDARY_CONTROL") {
|
||||
attributeState "battery", label:'Battery: ${currentValue}%', unit:"%"
|
||||
}
|
||||
}
|
||||
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "battery", label:'${currentValue}% battery', unit:"%"
|
||||
}
|
||||
valueTile("temperature", "device.temperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "temperature", label:'${currentValue}°', unit:"C"
|
||||
}
|
||||
valueTile("heatAlarm", "device.heatAlarm", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "clear", label:'TEMPERATURE OK', backgroundColor:"#ffffff"
|
||||
state "overheat detected", label:'OVERHEAT DETECTED', backgroundColor:"#ffffff"
|
||||
state "rapid temperature rise", label:'RAPID TEMP RISE', backgroundColor:"#ffffff"
|
||||
state "underheat detected", label:'UNDERHEAT DETECTED', backgroundColor:"#ffffff"
|
||||
}
|
||||
valueTile("tamper", "device.tamper", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "clear", label:'NO TAMPER', backgroundColor:"#ffffff"
|
||||
state "detected", label:'TAMPER DETECTED', backgroundColor:"#ffffff"
|
||||
}
|
||||
|
||||
main "smoke"
|
||||
details(["smoke","temperature"])
|
||||
}
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
setConfigured("false") //wait until the next time device wakeup to send configure command
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
log.debug "parse() >> description: $description"
|
||||
def result = null
|
||||
if (description.startsWith("Err 106")) {
|
||||
log.debug "parse() >> Err 106"
|
||||
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.")
|
||||
} else if (description != "updated") {
|
||||
log.debug "parse() >> zwave.parse(description)"
|
||||
def cmd = zwave.parse(description, [0x31: 5, 0x71: 3, 0x84: 1])
|
||||
if (cmd) {
|
||||
result = zwaveEvent(cmd)
|
||||
}
|
||||
}
|
||||
log.debug "After zwaveEvent(cmd) >> Parsed '${description}' to ${result.inspect()}"
|
||||
return result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) {
|
||||
log.info "Executing zwaveEvent 86 (VersionV1): 12 (VersionReport) with cmd: $cmd"
|
||||
def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}"
|
||||
updateDataValue("fw", fw)
|
||||
def text = "$device.displayName: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}"
|
||||
createEvent(descriptionText: text, isStateChange: false)
|
||||
}
|
||||
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||
def map = [ name: "battery", unit: "%" ]
|
||||
if (cmd.batteryLevel == 0xFF) {
|
||||
map.value = 1
|
||||
map.descriptionText = "${device.displayName} battery is low"
|
||||
map.isStateChange = true
|
||||
} else {
|
||||
map.value = cmd.batteryLevel
|
||||
}
|
||||
setConfigured("true") //when battery is reported back meaning configuration is done
|
||||
//Store time of last battery update so we don't ask every wakeup, see WakeUpNotification handler
|
||||
state.lastbatt = now()
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationBusy cmd) {
|
||||
def msg = cmd.status == 0 ? "try again later" :
|
||||
cmd.status == 1 ? "try again in $cmd.waitTime seconds" :
|
||||
cmd.status == 2 ? "request queued" : "sorry"
|
||||
createEvent(displayed: true, descriptionText: "$device.displayName is busy, $msg")
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) {
|
||||
createEvent(displayed: true, descriptionText: "$device.displayName rejected the last request")
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||
setSecured()
|
||||
def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 5, 0x71: 3, 0x84: 1])
|
||||
if (encapsulatedCommand) {
|
||||
log.debug "command: 98 (Security) 81(SecurityMessageEncapsulation) encapsulatedCommand: $encapsulatedCommand"
|
||||
zwaveEvent(encapsulatedCommand)
|
||||
} else {
|
||||
log.warn "Unable to extract encapsulated cmd from $cmd"
|
||||
createEvent(descriptionText: cmd.toString())
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) {
|
||||
log.info "Executing zwaveEvent 98 (SecurityV1): 03 (SecurityCommandsSupportedReport) with cmd: $cmd"
|
||||
setSecured()
|
||||
log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device"
|
||||
if (getDataValue("MSR")?.startsWith("010F-0C02")){
|
||||
response(configure()) //configure device using SmartThings default settings
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) {
|
||||
log.info "Executing zwaveEvent 98 (SecurityV1): 07 (NetworkKeyVerify) with cmd: $cmd (node is securely included)"
|
||||
createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful", isStateChange: true, displayed: true)
|
||||
//after device securely joined the network, call configure() to config device
|
||||
setSecured()
|
||||
log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device"
|
||||
if (getDataValue("MSR")?.startsWith("010F-0C02")){
|
||||
response(configure()) //configure device using SmartThings default settings
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
|
||||
log.info "Executing zwaveEvent 71 (NotificationV3): 05 (NotificationReport) with cmd: $cmd"
|
||||
def result = []
|
||||
if (cmd.notificationType == 7) {
|
||||
switch (cmd.event) {
|
||||
case 0:
|
||||
result << createEvent(name: "tamper", value: "clear", displayed: false)
|
||||
break
|
||||
case 3:
|
||||
result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName casing was opened")
|
||||
break
|
||||
}
|
||||
} else if (cmd.notificationType == 1) { //Smoke Alarm (V2)
|
||||
log.debug "notificationv3.NotificationReport: for Smoke Alarm (V2)"
|
||||
result << smokeAlarmEvent(cmd.event)
|
||||
} else if (cmd.notificationType == 4) { // Heat Alarm (V2)
|
||||
log.debug "notificationv3.NotificationReport: for Heat Alarm (V2)"
|
||||
result << heatAlarmEvent(cmd.event)
|
||||
} else {
|
||||
log.warn "Need to handle this cmd.notificationType: ${cmd.notificationType}"
|
||||
result << createEvent(descriptionText: cmd.toString(), isStateChange: false)
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
def smokeAlarmEvent(value) {
|
||||
log.debug "smokeAlarmEvent(value): $value"
|
||||
def map = [name: "smoke"]
|
||||
if (value == 1 || value == 2) {
|
||||
map.value = "detected"
|
||||
map.descriptionText = "$device.displayName detected smoke"
|
||||
} else if (value == 0) {
|
||||
map.value = "clear"
|
||||
map.descriptionText = "$device.displayName is clear (no smoke)"
|
||||
} else if (value == 3) {
|
||||
map.value = "tested"
|
||||
map.descriptionText = "$device.displayName smoke alarm test"
|
||||
} else if (value == 4) {
|
||||
map.value = "replacement required"
|
||||
map.descriptionText = "$device.displayName replacement required"
|
||||
} else {
|
||||
map.value = "unknown"
|
||||
map.descriptionText = "$device.displayName unknown event"
|
||||
}
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def heatAlarmEvent(value) {
|
||||
log.debug "heatAlarmEvent(value): $value"
|
||||
def map = [name: "heatAlarm"]
|
||||
if (value == 1 || value == 2) {
|
||||
map.value = "overheat detected"
|
||||
map.descriptionText = "$device.displayName overheat detected"
|
||||
} else if (value == 0) {
|
||||
map.value = "clear"
|
||||
map.descriptionText = "$device.displayName heat alarm cleared (no overheat)"
|
||||
} else if (value == 3 || value == 4) {
|
||||
map.value = "rapid temperature rise"
|
||||
map.descriptionText = "$device.displayName rapid temperature rise"
|
||||
} else if (value == 5 || value == 6) {
|
||||
map.value = "underheat detected"
|
||||
map.descriptionText = "$device.displayName underheat detected"
|
||||
} else {
|
||||
map.value = "unknown"
|
||||
map.descriptionText = "$device.displayName unknown event"
|
||||
}
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) {
|
||||
log.info "Executing zwaveEvent 84 (WakeUpV1): 07 (WakeUpNotification) with cmd: $cmd"
|
||||
log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device"
|
||||
def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)]
|
||||
def cmds = []
|
||||
/* check MSR = "manufacturerId-productTypeId" to make sure configuration commands are sent to the right model */
|
||||
if (!isConfigured() && getDataValue("MSR")?.startsWith("010F-0C02")) {
|
||||
result << response(configure()) // configure a newly joined device or joined device with preference update
|
||||
} else {
|
||||
//Only ask for battery if we haven't had a BatteryReport in a while
|
||||
if (!state.lastbatt || (new Date().time) - state.lastbatt > 24*60*60*1000) {
|
||||
log.debug("Device has been configured sending >> batteryGet()")
|
||||
cmds << zwave.securityV1.securityMessageEncapsulation().encapsulate(zwave.batteryV1.batteryGet()).format()
|
||||
cmds << "delay 1200"
|
||||
}
|
||||
log.debug("Device has been configured sending >> wakeUpNoMoreInformation()")
|
||||
cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format()
|
||||
result << response(cmds) //tell device back to sleep
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) {
|
||||
log.info "Executing zwaveEvent 31 (SensorMultilevelV5): 05 (SensorMultilevelReport) with cmd: $cmd"
|
||||
def map = [:]
|
||||
switch (cmd.sensorType) {
|
||||
case 1:
|
||||
map.name = "temperature"
|
||||
def cmdScale = cmd.scale == 1 ? "F" : "C"
|
||||
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision)
|
||||
map.unit = getTemperatureScale()
|
||||
break
|
||||
default:
|
||||
map.descriptionText = cmd.toString()
|
||||
}
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) {
|
||||
log.info "Executing zwaveEvent 5A (DeviceResetLocallyV1) : 01 (DeviceResetLocallyNotification) with cmd: $cmd"
|
||||
createEvent(descriptionText: cmd.toString(), isStateChange: true, displayed: true)
|
||||
}
|
||||
|
||||
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 result = []
|
||||
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
|
||||
updateDataValue("MSR", msr)
|
||||
log.debug "After device is securely joined, send commands to update tiles"
|
||||
result << zwave.batteryV1.batteryGet()
|
||||
result << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01)
|
||||
result << zwave.wakeUpV1.wakeUpNoMoreInformation()
|
||||
[[descriptionText:"${device.displayName} MSR report"], response(commands(result, 5000))]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) {
|
||||
def result = []
|
||||
if (cmd.nodeId.any { it == zwaveHubNodeId }) {
|
||||
result << createEvent(descriptionText: "$device.displayName is associated in group ${cmd.groupingIdentifier}")
|
||||
} else if (cmd.groupingIdentifier == 1) {
|
||||
result << createEvent(descriptionText: "Associating $device.displayName in group ${cmd.groupingIdentifier}")
|
||||
result << response(zwave.associationV1.associationSet(groupingIdentifier:cmd.groupingIdentifier, nodeId:zwaveHubNodeId))
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
log.warn "General zwaveEvent cmd: ${cmd}"
|
||||
createEvent(descriptionText: cmd.toString(), isStateChange: false)
|
||||
}
|
||||
|
||||
def configure() {
|
||||
// This sensor joins as a secure device if you tripple-click the button to include it
|
||||
log.debug "configure() >> isSecured() : ${isSecured()}"
|
||||
if (!isSecured()) {
|
||||
log.debug "Fibaro smoke sensor not sending configure until secure"
|
||||
return []
|
||||
} else {
|
||||
log.info "${device.displayName} is configuring its settings"
|
||||
def request = []
|
||||
|
||||
//1. configure wakeup interval : available: 0, 4200s-65535s, device default 21600s(6hr)
|
||||
request += zwave.wakeUpV1.wakeUpIntervalSet(seconds:6*3600, nodeid:zwaveHubNodeId)
|
||||
|
||||
//2. Smoke Sensitivity 3 levels: 1-HIGH , 2-MEDIUM (default), 3-LOW
|
||||
if (smokeSensorSensitivity && smokeSensorSensitivity != "null") {
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 1, size: 1,
|
||||
scaledConfigurationValue:
|
||||
smokeSensorSensitivity == "High" ? 1 :
|
||||
smokeSensorSensitivity == "Medium" ? 2 :
|
||||
smokeSensorSensitivity == "Low" ? 3 : 2)
|
||||
}
|
||||
//3. Z-Wave notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable
|
||||
if (zwaveNotificationStatus && zwaveNotificationStatus != "null"){
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: notificationOptionValueMap[zwaveNotificationStatus] ?: 0)
|
||||
}
|
||||
//4. Visual indicator notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable, 4-lack of range notification
|
||||
if (visualIndicatorNotificationStatus && visualIndicatorNotificationStatus != "null") {
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: notificationOptionValueMap[visualIndicatorNotificationStatus] ?: 0)
|
||||
}
|
||||
//5. Sound notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable, 4-lack of range notification
|
||||
if (soundNotificationStatus && soundNotificationStatus != "null") {
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, scaledConfigurationValue: notificationOptionValueMap[soundNotificationStatus] ?: 0)
|
||||
}
|
||||
//6. Temperature report interval: 0-report inactive, 1-8640 (multiply by 10 secs) [10s-24hr], default 180 (30 minutes)
|
||||
if (temperatureReportInterval && temperatureReportInterval != "null") {
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 20, size: 2, scaledConfigurationValue: timeOptionValueMap[temperatureReportInterval] ?: 180)
|
||||
} else { //send SmartThings default configuration
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 20, size: 2, scaledConfigurationValue: 180)
|
||||
}
|
||||
//7. Temperature report hysteresis: 1-100 (in 0.1C step) [0.1C - 10C], default 10 (1 C)
|
||||
if (temperatureReportHysteresis && temperatureReportHysteresis != null) {
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 21, size: 1, scaledConfigurationValue: temperatureReportHysteresis < 1 ? 1 : temperatureReportHysteresis > 100 ? 100 : temperatureReportHysteresis)
|
||||
}
|
||||
//8. Temperature threshold: 1-100 (C), default 55 (C)
|
||||
if (temperatureThreshold && temperatureThreshold != null) {
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 30, size: 1, scaledConfigurationValue: temperatureThreshold < 1 ? 1 : temperatureThreshold > 100 ? 100 : temperatureThreshold)
|
||||
}
|
||||
//9. Excess temperature signaling interval: 1-8640 (multiply by 10 secs) [10s-24hr], default 180 (30 minutes)
|
||||
if (excessTemperatureSignalingInterval && excessTemperatureSignalingInterval != "null") {
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 31, size: 2, scaledConfigurationValue: timeOptionValueMap[excessTemperatureSignalingInterval] ?: 180)
|
||||
} else { //send SmartThings default configuration
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 31, size: 2, scaledConfigurationValue: 180)
|
||||
}
|
||||
//10. Lack of Z-Wave range indication interval: 1-8640 (multiply by 10 secs) [10s-24hr], default 2160 (6 hours)
|
||||
if (lackOfZwaveRangeIndicationInterval && lackOfZwaveRangeIndicationInterval != "null") {
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 32, size: 2, scaledConfigurationValue: timeOptionValueMap[lackOfZwaveRangeIndicationInterval] ?: 2160)
|
||||
} else {
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 32, size: 2, scaledConfigurationValue: 2160)
|
||||
}
|
||||
//11. get battery level when device is paired
|
||||
request += zwave.batteryV1.batteryGet()
|
||||
|
||||
//12. get temperature reading from device
|
||||
request += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01)
|
||||
|
||||
commands(request) + ["delay 10000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]
|
||||
}
|
||||
}
|
||||
|
||||
private def getTimeOptionValueMap() { [
|
||||
"5 minutes" : 30,
|
||||
"15 minutes" : 90,
|
||||
"30 minutes" : 180,
|
||||
"1 hour" : 360,
|
||||
"6 hours" : 2160,
|
||||
"12 hours" : 4320,
|
||||
"18 hours" : 6480,
|
||||
"24 hours" : 8640,
|
||||
"reports inactive" : 0,
|
||||
]}
|
||||
|
||||
private def getNotificationOptionValueMap() { [
|
||||
"disabled" : 0,
|
||||
"casing opened" : 1,
|
||||
"exceeding temperature threshold" : 2,
|
||||
"lack of Z-Wave range" : 4,
|
||||
"all notifications" : 7,
|
||||
]}
|
||||
|
||||
private command(physicalgraph.zwave.Command cmd) {
|
||||
if (isSecured()) {
|
||||
log.info "Sending secured command: ${cmd}"
|
||||
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||
} else {
|
||||
log.info "Sending unsecured command: ${cmd}"
|
||||
cmd.format()
|
||||
}
|
||||
}
|
||||
|
||||
private commands(commands, delay=200) {
|
||||
log.info "inside commands: ${commands}"
|
||||
delayBetween(commands.collect{ command(it) }, delay)
|
||||
}
|
||||
|
||||
private setConfigured(configure) {
|
||||
updateDataValue("configured", configure)
|
||||
}
|
||||
private isConfigured() {
|
||||
getDataValue("configured") == "true"
|
||||
}
|
||||
private setSecured() {
|
||||
updateDataValue("secured", "true")
|
||||
}
|
||||
private isSecured() {
|
||||
getDataValue("secured") == "true"
|
||||
}
|
||||
@@ -19,7 +19,6 @@ metadata {
|
||||
capability "Sensor"
|
||||
|
||||
fingerprint deviceId: "0x1000", inClusters: "0x25,0x72,0x86,0x71,0x22,0x70"
|
||||
fingerprint deviceId: "0x1006", inClusters: "0x25"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
|
||||
@@ -50,7 +50,7 @@ metadata {
|
||||
capability "Switch Level"
|
||||
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
|
||||
@@ -317,7 +317,7 @@ def setLevel(value) {
|
||||
state.trigger = "setLevel"
|
||||
state.lvl = "${level}"
|
||||
|
||||
if (dimRate) {
|
||||
if (dimRate && (state?.rate != null)) {
|
||||
cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} ${state.rate}}"
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -26,8 +26,6 @@ metadata {
|
||||
capability "Actuator"
|
||||
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
|
||||
|
||||
@@ -26,8 +26,6 @@ metadata {
|
||||
capability "Actuator"
|
||||
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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -25,7 +25,7 @@ metadata {
|
||||
tiles {
|
||||
standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) {
|
||||
state("present", labelIcon:"st.presence.tile.mobile-present", backgroundColor:"#53a7c0")
|
||||
state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#ffffff")
|
||||
state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#ebeef2")
|
||||
}
|
||||
main "presence"
|
||||
details "presence"
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
that issue by using state variables
|
||||
*/
|
||||
|
||||
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
|
||||
|
||||
metadata {
|
||||
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)
|
||||
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"
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ metadata {
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS2.png"
|
||||
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS1.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS2.jpg"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,9 @@ metadata {
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion2.png",
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion3.png"
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion1.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion2.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion3.jpg"
|
||||
])
|
||||
}
|
||||
section {
|
||||
|
||||
@@ -54,10 +54,10 @@
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi2.png",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi3.png",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi4.png"
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi1.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi2.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi3.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi4.jpg"
|
||||
])
|
||||
}
|
||||
section {
|
||||
@@ -346,8 +346,8 @@ def getTemperature(value) {
|
||||
log.debug "Acceleration"
|
||||
def name = "acceleration"
|
||||
def value = numValue.endsWith("1") ? "active" : "inactive"
|
||||
//def linkText = getLinkText(device)
|
||||
def descriptionText = "was $value"
|
||||
def linkText = getLinkText(device)
|
||||
def descriptionText = "$linkText was $value"
|
||||
def isStateChange = isStateChange(device, name, value)
|
||||
[
|
||||
name: name,
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
* 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 {
|
||||
definition (name: "Sylvania Ultra iQ", namespace:"smartthings", author: "SmartThings") {
|
||||
capability "Switch Level"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
metadata {
|
||||
definition (name: "Color Control Capability", namespace: "capabilities", author: "SmartThings") {
|
||||
definition (name: "Simulated Color Control", namespace: "smartthings/testing", author: "SmartThings") {
|
||||
capability "Color Control"
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
* 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 {
|
||||
definition (name: "WeMo Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
@@ -25,7 +27,6 @@ metadata {
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,FF00", outClusters: "0019"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
|
||||
@@ -25,6 +25,8 @@ metadata {
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
|
||||
attribute "currentIP", "string"
|
||||
|
||||
command "subscribe"
|
||||
command "resubscribe"
|
||||
command "unsubscribe"
|
||||
@@ -34,21 +36,36 @@ metadata {
|
||||
// simulator metadata
|
||||
simulator {}
|
||||
|
||||
// UI tile definitions
|
||||
tiles {
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
state "turningOn", label:'${name}', icon:"st.switches.switch.on", backgroundColor:"#79b821"
|
||||
state "turningOff", label:'${name}', 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"
|
||||
}
|
||||
// UI tile definitions
|
||||
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.Home.home30", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
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"
|
||||
attributeState "offline", label:'${name}', icon:"st.Home.home30", backgroundColor:"#ff0000"
|
||||
}
|
||||
tileAttribute ("currentIP", key: "SECONDARY_CONTROL") {
|
||||
attributeState "currentIP", label: ''
|
||||
}
|
||||
}
|
||||
|
||||
main "switch"
|
||||
details (["switch", "refresh"])
|
||||
}
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
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
|
||||
@@ -68,6 +85,7 @@ def parse(String description) {
|
||||
def result = []
|
||||
def bodyString = msg.body
|
||||
if (bodyString) {
|
||||
unschedule("setOffline")
|
||||
def body = new XmlSlurper().parseText(bodyString)
|
||||
|
||||
if (body?.property?.TimeSyncRequest?.text()) {
|
||||
@@ -78,13 +96,14 @@ def parse(String description) {
|
||||
} else if (body?.property?.BinaryState?.text()) {
|
||||
def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
|
||||
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()) {
|
||||
log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}"
|
||||
} else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) {
|
||||
def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
|
||||
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")
|
||||
}
|
||||
|
||||
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() {
|
||||
def ip = getDataValue("ip")
|
||||
def port = getDataValue("port")
|
||||
@@ -195,6 +206,8 @@ def subscribe(ip, 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"
|
||||
@@ -259,6 +272,8 @@ User-Agent: CyberGarage-HTTP/1.0
|
||||
|
||||
def poll() {
|
||||
log.debug "Executing 'poll'"
|
||||
if (device.currentValue("currentIP") != "Offline")
|
||||
runIn(10, setOffline)
|
||||
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
||||
Content-Length: 277
|
||||
@@ -274,3 +289,15 @@ User-Agent: CyberGarage-HTTP/1.0
|
||||
</s:Body>
|
||||
</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 "Sensor"
|
||||
|
||||
attribute "currentIP", "string"
|
||||
|
||||
command "subscribe"
|
||||
command "resubscribe"
|
||||
command "unsubscribe"
|
||||
@@ -31,17 +33,30 @@
|
||||
}
|
||||
|
||||
// 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) {
|
||||
state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0")
|
||||
state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff")
|
||||
}
|
||||
standardTile("refresh", "device.motion", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
state("offline", label:'${name}', icon:"st.motion.motion.inactive", 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 "motion"
|
||||
details (["motion", "refresh"])
|
||||
details (["rich-control", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +77,7 @@ def parse(String description) {
|
||||
def result = []
|
||||
def bodyString = msg.body
|
||||
if (bodyString) {
|
||||
unschedule("setOffline")
|
||||
def body = new XmlSlurper().parseText(bodyString)
|
||||
|
||||
if (body?.property?.TimeSyncRequest?.text()) {
|
||||
@@ -72,7 +88,7 @@ def parse(String description) {
|
||||
} else if (body?.property?.BinaryState?.text()) {
|
||||
def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "active" : "inactive"
|
||||
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()) {
|
||||
log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}"
|
||||
}
|
||||
@@ -91,14 +107,6 @@ private getCallBackAddress() {
|
||||
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() {
|
||||
def ip = getDataValue("ip")
|
||||
def port = getDataValue("port")
|
||||
@@ -125,6 +133,8 @@ def refresh() {
|
||||
////////////////////////////
|
||||
def 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
|
||||
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
||||
Content-Length: 277
|
||||
@@ -165,7 +175,9 @@ def subscribe(ip, port) {
|
||||
def existingPort = getDataValue("port")
|
||||
if (ip && ip != existingIp) {
|
||||
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) {
|
||||
log.debug "Updating port from $existingPort to $port"
|
||||
@@ -226,3 +238,15 @@ User-Agent: CyberGarage-HTTP/1.0
|
||||
</s:Envelope>
|
||||
""", 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
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* Wemo Switch
|
||||
* Wemo Switch
|
||||
*
|
||||
* Author: superuser
|
||||
* Date: 2013-10-11
|
||||
* Author: Juan Risso (SmartThings)
|
||||
* Date: 2015-10-11
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Wemo Switch", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Switch"
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
definition (name: "Wemo Switch", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Switch"
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
|
||||
command "subscribe"
|
||||
command "resubscribe"
|
||||
command "unsubscribe"
|
||||
}
|
||||
attribute "currentIP", "string"
|
||||
|
||||
// simulator metadata
|
||||
simulator {}
|
||||
command "subscribe"
|
||||
command "resubscribe"
|
||||
command "unsubscribe"
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles {
|
||||
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"
|
||||
}
|
||||
// simulator metadata
|
||||
simulator {}
|
||||
|
||||
main "switch"
|
||||
details (["switch", "refresh"])
|
||||
}
|
||||
// UI tile definitions
|
||||
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
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
log.debug "Parsing '${description}'"
|
||||
|
||||
def msg = parseLanMessage(description)
|
||||
def headerString = msg.header
|
||||
def msg = parseLanMessage(description)
|
||||
def headerString = msg.header
|
||||
|
||||
if (headerString?.contains("SID: uuid:")) {
|
||||
def sid = (headerString =~ /SID: uuid:.*/) ? ( headerString =~ /SID: uuid:.*/)[0] : "0"
|
||||
sid -= "SID: uuid:".trim()
|
||||
if (headerString?.contains("SID: uuid:")) {
|
||||
def sid = (headerString =~ /SID: uuid:.*/) ? ( headerString =~ /SID: uuid:.*/)[0] : "0"
|
||||
sid -= "SID: uuid:".trim()
|
||||
|
||||
updateDataValue("subscriptionId", sid)
|
||||
}
|
||||
updateDataValue("subscriptionId", sid)
|
||||
}
|
||||
|
||||
def result = []
|
||||
def bodyString = msg.body
|
||||
if (bodyString) {
|
||||
def body = new XmlSlurper().parseText(bodyString)
|
||||
|
||||
if (body?.property?.TimeSyncRequest?.text()) {
|
||||
log.trace "Got TimeSyncRequest"
|
||||
result << timeSyncResponse()
|
||||
} else if (body?.Body?.SetBinaryStateResponse?.BinaryState?.text()) {
|
||||
log.trace "Got SetBinaryStateResponse = ${body?.Body?.SetBinaryStateResponse?.BinaryState?.text()}"
|
||||
} else if (body?.property?.BinaryState?.text()) {
|
||||
def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
|
||||
log.trace "Notify: BinaryState = ${value}"
|
||||
result << createEvent(name: "switch", value: value)
|
||||
} else if (body?.property?.TimeZoneNotification?.text()) {
|
||||
log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}"
|
||||
} else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) {
|
||||
def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
|
||||
log.trace "GetBinaryResponse: BinaryState = ${value}"
|
||||
result << createEvent(name: "switch", value: value)
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
def result = []
|
||||
def bodyString = msg.body
|
||||
if (bodyString) {
|
||||
unschedule("setOffline")
|
||||
def body = new XmlSlurper().parseText(bodyString)
|
||||
if (body?.property?.TimeSyncRequest?.text()) {
|
||||
log.trace "Got TimeSyncRequest"
|
||||
result << timeSyncResponse()
|
||||
} else if (body?.Body?.SetBinaryStateResponse?.BinaryState?.text()) {
|
||||
log.trace "Got SetBinaryStateResponse = ${body?.Body?.SetBinaryStateResponse?.BinaryState?.text()}"
|
||||
} else if (body?.property?.BinaryState?.text()) {
|
||||
def value = body?.property?.BinaryState?.text().substring(0, 1).toInteger() == 0 ? "off" : "on"
|
||||
log.trace "Notify: BinaryState = ${value}, ${body.property.BinaryState}"
|
||||
def dispaux = device.currentValue("switch") != value
|
||||
result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux)
|
||||
} else if (body?.property?.TimeZoneNotification?.text()) {
|
||||
log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}"
|
||||
} else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) {
|
||||
def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().substring(0, 1).toInteger() == 0 ? "off" : "on"
|
||||
log.trace "GetBinaryResponse: BinaryState = ${value}, ${body.property.BinaryState}"
|
||||
log.info "Connection: ${device.currentValue("connection")}"
|
||||
if (device.currentValue("currentIP") == "Offline") {
|
||||
def ipvalue = convertHexToIP(getDataValue("ip"))
|
||||
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() {
|
||||
// This is essentially System.currentTimeMillis()/1000, but System is disallowed by the sandbox.
|
||||
((new GregorianCalendar().time.time / 1000l).toInteger()).toString()
|
||||
// This is essentially System.currentTimeMillis()/1000, but System is disallowed by the sandbox.
|
||||
((new GregorianCalendar().time.time / 1000l).toInteger()).toString()
|
||||
}
|
||||
|
||||
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)
|
||||
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(".")
|
||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||
}
|
||||
|
||||
private getHostAddress() {
|
||||
def ip = getDataValue("ip")
|
||||
def port = getDataValue("port")
|
||||
|
||||
if (!ip || !port) {
|
||||
def parts = device.deviceNetworkId.split(":")
|
||||
if (parts.length == 2) {
|
||||
ip = parts[0]
|
||||
port = parts[1]
|
||||
} else {
|
||||
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}"
|
||||
return convertHexToIP(ip) + ":" + convertHexToInt(port)
|
||||
def ip = getDataValue("ip")
|
||||
def port = getDataValue("port")
|
||||
if (!ip || !port) {
|
||||
def parts = device.deviceNetworkId.split(":")
|
||||
if (parts.length == 2) {
|
||||
ip = parts[0]
|
||||
port = parts[1]
|
||||
} else {
|
||||
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}"
|
||||
return convertHexToIP(ip) + ":" + convertHexToInt(port)
|
||||
}
|
||||
|
||||
|
||||
def on() {
|
||||
log.debug "Executing 'on'"
|
||||
sendEvent(name: "switch", value: "on")
|
||||
log.debug "Executing 'on'"
|
||||
def turnOn = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||
SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState"
|
||||
Host: ${getHostAddress()}
|
||||
@@ -133,17 +155,16 @@ Content-Length: 333
|
||||
<?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:Body>
|
||||
<m:SetBinaryState xmlns:m="urn:Belkin:service:basicevent:1">
|
||||
<m:SetBinaryState xmlns:m="urn:Belkin:service:basicevent:1">
|
||||
<BinaryState>1</BinaryState>
|
||||
</m:SetBinaryState>
|
||||
</m:SetBinaryState>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>""", physicalgraph.device.Protocol.LAN)
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "Executing 'off'"
|
||||
sendEvent(name: "switch", value: "off")
|
||||
def turnOff = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||
log.debug "Executing 'off'"
|
||||
def turnOff = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||
SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState"
|
||||
Host: ${getHostAddress()}
|
||||
Content-Type: text/xml
|
||||
@@ -152,36 +173,13 @@ Content-Length: 333
|
||||
<?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:Body>
|
||||
<m:SetBinaryState xmlns:m="urn:Belkin:service:basicevent:1">
|
||||
<m:SetBinaryState xmlns:m="urn:Belkin:service:basicevent:1">
|
||||
<BinaryState>0</BinaryState>
|
||||
</m:SetBinaryState>
|
||||
</m:SetBinaryState>
|
||||
</SOAP-ENV:Body>
|
||||
</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) {
|
||||
log.debug "Executing 'subscribe()'"
|
||||
def address = getCallBackAddress()
|
||||
@@ -200,27 +198,30 @@ def subscribe() {
|
||||
subscribe(getHostAddress())
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if (port && port != existingPort) {
|
||||
log.debug "Updating port from $existingPort to $port"
|
||||
updateDataValue("port", port)
|
||||
}
|
||||
def refresh() {
|
||||
log.debug "Executing WeMo Switch 'subscribe', then 'timeSyncResponse', then 'poll'"
|
||||
[subscribe(), timeSyncResponse(), poll()]
|
||||
}
|
||||
|
||||
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}")
|
||||
}
|
||||
|
||||
////////////////////////////
|
||||
def resubscribe() {
|
||||
log.debug "Executing 'resubscribe()'"
|
||||
|
||||
def sid = getDeviceDataByName("subscriptionId")
|
||||
|
||||
log.debug "Executing 'resubscribe()'"
|
||||
def sid = getDeviceDataByName("subscriptionId")
|
||||
new physicalgraph.device.HubAction("""SUBSCRIBE /upnp/event/basicevent1 HTTP/1.1
|
||||
HOST: ${getHostAddress()}
|
||||
SID: uuid:${sid}
|
||||
@@ -228,12 +229,11 @@ TIMEOUT: Second-5400
|
||||
|
||||
|
||||
""", physicalgraph.device.Protocol.LAN)
|
||||
|
||||
}
|
||||
|
||||
////////////////////////////
|
||||
|
||||
def unsubscribe() {
|
||||
def sid = getDeviceDataByName("subscriptionId")
|
||||
def sid = getDeviceDataByName("subscriptionId")
|
||||
new physicalgraph.device.HubAction("""UNSUBSCRIBE publisher path HTTP/1.1
|
||||
HOST: ${getHostAddress()}
|
||||
SID: uuid:${sid}
|
||||
@@ -242,7 +242,7 @@ SID: uuid:${sid}
|
||||
""", physicalgraph.device.Protocol.LAN)
|
||||
}
|
||||
|
||||
////////////////////////////
|
||||
|
||||
//TODO: Use UTC Timezone
|
||||
def timeSyncResponse() {
|
||||
log.debug "Executing 'timeSyncResponse()'"
|
||||
@@ -267,9 +267,15 @@ User-Agent: CyberGarage-HTTP/1.0
|
||||
""", 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() {
|
||||
log.debug "Executing 'poll'"
|
||||
if (device.currentValue("currentIP") != "Offline")
|
||||
runIn(10, setOffline)
|
||||
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
||||
Content-Length: 277
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "ZigBee Dimmer Power", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Power Meter"
|
||||
capability "Sensor"
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
|
||||
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, 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) {
|
||||
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.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel"
|
||||
}
|
||||
tileAttribute ("power", key: "SECONDARY_CONTROL") {
|
||||
attributeState "power", label:'${currentValue} W'
|
||||
}
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
main "switch"
|
||||
details(["switch", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def resultMap = zigbee.getKnownDescription(description)
|
||||
if (resultMap) {
|
||||
log.info resultMap
|
||||
if (resultMap.type == "update") {
|
||||
log.info "$device updates: ${resultMap.value}"
|
||||
}
|
||||
else if (resultMap.type == "power") {
|
||||
def powerValue
|
||||
if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power
|
||||
powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration
|
||||
sendEvent(name: "power", value: powerValue)
|
||||
}
|
||||
}
|
||||
else {
|
||||
sendEvent(name: resultMap.type, value: resultMap.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.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh()
|
||||
}
|
||||
@@ -11,133 +11,80 @@
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "ZigBee Dimmer", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Switch Level"
|
||||
capability "Actuator"
|
||||
capability "Switch"
|
||||
capability "Configuration"
|
||||
capability "Sensor"
|
||||
capability "Refresh"
|
||||
definition (name: "ZigBee Dimmer", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05", outClusters: "0019"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
// status messages
|
||||
status "on": "on/off: 1"
|
||||
status "off": "on/off: 0"
|
||||
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"
|
||||
}
|
||||
|
||||
// reply messages
|
||||
reply "zcl on-off on": "on/off: 1"
|
||||
reply "zcl on-off off": "on/off: 0"
|
||||
}
|
||||
|
||||
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.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel"
|
||||
}
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
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"
|
||||
details(["switch", "refresh", "level", "levelSliderControl"])
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
main "switch"
|
||||
details(["switch", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.info description
|
||||
if (description?.startsWith("catchall:")) {
|
||||
def msg = zigbee.parse(description)
|
||||
log.trace msg
|
||||
log.trace "data: $msg.data"
|
||||
}
|
||||
else {
|
||||
def name = description?.startsWith("on/off: ") ? "switch" : null
|
||||
def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null
|
||||
def result = createEvent(name: name, value: value)
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
}
|
||||
}
|
||||
log.debug "description is $description"
|
||||
|
||||
// Commands to device
|
||||
def on() {
|
||||
log.debug "on()"
|
||||
sendEvent(name: "switch", value: "on")
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}"
|
||||
def resultMap = zigbee.getKnownDescription(description)
|
||||
if (resultMap) {
|
||||
log.info resultMap
|
||||
if (resultMap.type == "update") {
|
||||
log.info "$device updates: ${resultMap.value}"
|
||||
}
|
||||
else {
|
||||
sendEvent(name: resultMap.type, value: resultMap.value)
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug zigbee.parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "off()"
|
||||
sendEvent(name: "switch", value: "off")
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
|
||||
zigbee.off()
|
||||
}
|
||||
|
||||
def on() {
|
||||
zigbee.on()
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
log.trace "setLevel($value)"
|
||||
def cmds = []
|
||||
|
||||
if (value == 0) {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
|
||||
}
|
||||
else if (device.latestValue("switch") == "off") {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}"
|
||||
|
||||
}
|
||||
|
||||
sendEvent(name: "level", value: value)
|
||||
def level = hexString(Math.round(value * 255/100))
|
||||
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}"
|
||||
|
||||
//log.debug cmds
|
||||
cmds
|
||||
zigbee.setLevel(value)
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
[
|
||||
"st wattr 0x${device.deviceNetworkId} 1 6 0", "delay 200",
|
||||
"st wattr 0x${device.deviceNetworkId} 1 8 0"
|
||||
]
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
|
||||
/*log.debug "binding to switch and level control cluster"
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}"
|
||||
]
|
||||
*/
|
||||
|
||||
//set transition time to 2 seconds. Not currently working.
|
||||
"st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {1400}"
|
||||
}
|
||||
|
||||
|
||||
|
||||
private hex(value, width=2) {
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() < width) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
}
|
||||
|
||||
184
devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy
Normal file
184
devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* ZigBee Lock
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "ZigBee Lock", namespace: "smartthings", author: "SmartThings")
|
||||
{
|
||||
capability "Actuator"
|
||||
capability "Lock"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Battery"
|
||||
capability "Configuration"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019",
|
||||
manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_5", deviceJoinName: "Kwikset 5-Button Deadbolt"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019",
|
||||
manufacturer: "Kwikset", model: "SMARTCODE_LEVER_5", deviceJoinName: "Kwikset 5-Button Lever"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019",
|
||||
manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_10", deviceJoinName: "Kwikset 10-Button Deadbolt"
|
||||
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"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019",
|
||||
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) {
|
||||
multiAttributeTile(name:"toggle", type:"generic", width:6, height:4){
|
||||
tileAttribute ("device.lock", key:"PRIMARY_CONTROL") {
|
||||
attributeState "locked", label:'locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#79b821", nextState:"unlocking"
|
||||
attributeState "unlocked", label:'unlocked', action:"lock.lock", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff", nextState:"locking"
|
||||
attributeState "unknown", label:"unknown", action:"lock.lock", icon:"st.locks.lock.unknown", backgroundColor:"#ffffff", nextState:"locking"
|
||||
attributeState "locking", label:'locking', icon:"st.locks.lock.locked", backgroundColor:"#79b821"
|
||||
attributeState "unlocking", label:'unlocking', icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff"
|
||||
}
|
||||
}
|
||||
standardTile("lock", "device.lock", inactiveLabel:false, decoration:"flat", width:2, height:2) {
|
||||
state "default", label:'lock', action:"lock.lock", icon:"st.locks.lock.locked", nextState:"locking"
|
||||
}
|
||||
standardTile("unlock", "device.lock", inactiveLabel:false, decoration:"flat", width:2, height:2) {
|
||||
state "default", label:'unlock', action:"lock.unlock", icon:"st.locks.lock.unlocked", nextState:"unlocking"
|
||||
}
|
||||
valueTile("battery", "device.battery", inactiveLabel:false, decoration:"flat", width:2, height:2) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
standardTile("refresh", "device.lock", inactiveLabel:false, decoration:"flat", width:2, height:2) {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main "toggle"
|
||||
details(["toggle", "lock", "unlock", "battery", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Globals
|
||||
private getCLUSTER_POWER() { 0x0001 }
|
||||
private getCLUSTER_DOORLOCK() { 0x0101 }
|
||||
|
||||
private getDOORLOCK_CMD_LOCK_DOOR() { 0x00 }
|
||||
private getDOORLOCK_CMD_UNLOCK_DOOR() { 0x01 }
|
||||
private getDOORLOCK_ATTR_LOCKSTATE() { 0x0000 }
|
||||
private getPOWER_ATTR_BATTERY_PERCENTAGE_REMAINING() { 0x0021 }
|
||||
|
||||
private getTYPE_U8() { 0x20 }
|
||||
private getTYPE_ENUM8() { 0x30 }
|
||||
|
||||
// Public methods
|
||||
def installed() {
|
||||
log.trace "installed()"
|
||||
}
|
||||
|
||||
def uninstalled() {
|
||||
log.trace "uninstalled()"
|
||||
}
|
||||
|
||||
def configure() {
|
||||
/*
|
||||
def cmds =
|
||||
zigbee.configSetup("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}",
|
||||
"${TYPE_ENUM8}", 0, 3600, "{01}") +
|
||||
zigbee.configSetup("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}",
|
||||
"${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"
|
||||
return cmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
def cmds =
|
||||
zigbee.refreshData("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}") +
|
||||
zigbee.refreshData("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}")
|
||||
log.info "refresh() --- cmds: $cmds"
|
||||
return cmds
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
log.trace "parse() --- description: $description"
|
||||
|
||||
Map map = [:]
|
||||
if (description?.startsWith('read attr -')) {
|
||||
map = parseReportAttributeMessage(description)
|
||||
}
|
||||
|
||||
log.debug "parse() --- Parse returned $map"
|
||||
def result = map ? createEvent(map) : null
|
||||
return result
|
||||
}
|
||||
|
||||
// Lock capability commands
|
||||
def lock() {
|
||||
def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_LOCK_DOOR}", "{}")
|
||||
log.info "lock() -- cmds: $cmds"
|
||||
//return cmds
|
||||
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_LOCK_DOOR} {}"
|
||||
}
|
||||
|
||||
def unlock() {
|
||||
def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_UNLOCK_DOOR}", "{}")
|
||||
log.info "unlock() -- cmds: $cmds"
|
||||
//return cmds
|
||||
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_UNLOCK_DOOR} {}"
|
||||
}
|
||||
|
||||
// Private methods
|
||||
private Map parseReportAttributeMessage(String description) {
|
||||
log.trace "parseReportAttributeMessage() --- description: $description"
|
||||
|
||||
Map descMap = zigbee.parseDescriptionAsMap(description)
|
||||
|
||||
log.debug "parseReportAttributeMessage() --- descMap: $descMap"
|
||||
|
||||
Map resultMap = [:]
|
||||
if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) {
|
||||
resultMap.name = "battery"
|
||||
resultMap.value = Math.round(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}"
|
||||
}
|
||||
else if (descMap.clusterInt == CLUSTER_DOORLOCK && descMap.attrInt == DOORLOCK_ATTR_LOCKSTATE) {
|
||||
def value = Integer.parseInt(descMap.value, 16)
|
||||
resultMap.name = "lock"
|
||||
resultMap.putAll([0:["value":"unknown",
|
||||
"descriptionText":"Not fully locked"],
|
||||
1:["value":"locked"],
|
||||
2:["value":"unlocked"]].get(value,
|
||||
["value":"unknown",
|
||||
"descriptionText":"Unknown lock state"]))
|
||||
log.info "parseReportAttributeMessage() --- lock: ${resultMap.value}"
|
||||
}
|
||||
else {
|
||||
log.debug "parseReportAttributeMessage() --- ignoring attribute"
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "ZigBee Switch Power", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Power Meter"
|
||||
capability "Sensor"
|
||||
capability "Switch"
|
||||
|
||||
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,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) {
|
||||
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.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
tileAttribute ("power", key: "SECONDARY_CONTROL") {
|
||||
attributeState "power", label:'${currentValue} W'
|
||||
}
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
main "switch"
|
||||
details(["switch", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def resultMap = zigbee.getKnownDescription(description)
|
||||
if (resultMap) {
|
||||
log.info resultMap
|
||||
if (resultMap.type == "update") {
|
||||
log.info "$device updates: ${resultMap.value}"
|
||||
}
|
||||
else if (resultMap.type == "power") {
|
||||
def powerValue
|
||||
if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power
|
||||
powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration
|
||||
sendEvent(name: "power", value: powerValue)
|
||||
}
|
||||
}
|
||||
else {
|
||||
sendEvent(name: resultMap.type, value: resultMap.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 refresh() {
|
||||
zigbee.onOffRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
zigbee.onOffConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() + zigbee.onOffRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh()
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "ZigBee Switch", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Switch"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
// status messages
|
||||
status "on": "on/off: 1"
|
||||
status "off": "on/off: 0"
|
||||
|
||||
// reply messages
|
||||
reply "zcl on-off on": "on/off: 1"
|
||||
reply "zcl on-off off": "on/off: 0"
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
main "switch"
|
||||
details(["switch", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def resultMap = zigbee.getKnownDescription(description)
|
||||
if (resultMap) {
|
||||
log.info resultMap
|
||||
if (resultMap.type == "update") {
|
||||
log.info "$device updates: ${resultMap.value}"
|
||||
}
|
||||
else {
|
||||
sendEvent(name: resultMap.type, value: resultMap.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 refresh() {
|
||||
zigbee.onOffRefresh() + zigbee.onOffConfig()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
zigbee.onOffConfig() + zigbee.onOffRefresh()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Z-Wave Water Valve", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Valve"
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
|
||||
fingerprint deviceId: "0x1006", inClusters: "0x25"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
status "open": "command: 2503, payload: FF"
|
||||
status "close": "command: 2503, payload: 00"
|
||||
|
||||
// reply messages
|
||||
reply "2001FF,delay 100,2502": "command: 2503, payload: FF"
|
||||
reply "200100,delay 100,2502": "command: 2503, payload: 00"
|
||||
}
|
||||
|
||||
// tile definitions
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"valve", type: "generic", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.contact", key: "PRIMARY_CONTROL") {
|
||||
attributeState "open", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#53a7c0", nextState:"closing"
|
||||
attributeState "closed", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#e86d13", nextState:"opening"
|
||||
attributeState "opening", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#ffe71e"
|
||||
attributeState "closing", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#ffe71e"
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.contact", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main "valve"
|
||||
details(["valve","refresh"])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def updated() {
|
||||
response(refresh())
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
log.trace "parse description : $description"
|
||||
def result = null
|
||||
def cmd = zwave.parse(description, [0x20: 1])
|
||||
if (cmd) {
|
||||
result = createEvent(zwaveEvent(cmd))
|
||||
}
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
|
||||
def value = cmd.value == 0xFF ? "open" : cmd.value == 0x00 ? "closed" : "unknown"
|
||||
[name: "contact", value: value, descriptionText: "$device.displayName valve is $value"]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { //TODO should show MSR when device is discovered
|
||||
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)
|
||||
[descriptionText: "$device.displayName MSR: $msr", isStateChange: false]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) {
|
||||
[descriptionText: cmd.toString(), isStateChange: true, displayed: true]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
|
||||
def value = cmd.value == 0xFF ? "open" : cmd.value == 0x00 ? "closed" : "unknown"
|
||||
[name: "contact", value: value, descriptionText: "$device.displayName valve is $value"]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
[:] // Handles all Z-Wave commands we aren't interested in
|
||||
}
|
||||
|
||||
def open() {
|
||||
delayBetween([
|
||||
zwave.basicV1.basicSet(value: 0xFF).format(),
|
||||
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||
],10000) //wait for a water valve to be completely opened
|
||||
}
|
||||
|
||||
def close() {
|
||||
delayBetween([
|
||||
zwave.basicV1.basicSet(value: 0x00).format(),
|
||||
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||
],10000) //wait for a water valve to be completely closed
|
||||
}
|
||||
|
||||
def poll() {
|
||||
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "refresh() is called"
|
||||
def commands = [zwave.switchBinaryV1.switchBinaryGet().format()]
|
||||
if (getDataValue("MSR") == null) {
|
||||
commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
|
||||
}
|
||||
delayBetween(commands,100)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ metadata {
|
||||
definition (name: "Zen Thermostat", namespace: "zenwithin", author: "ZenWithin") {
|
||||
capability "Actuator"
|
||||
capability "Thermostat"
|
||||
capability "Temperature Measurement"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
|
||||
@@ -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.",
|
||||
category: "SmartThings Labs",
|
||||
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 {
|
||||
|
||||
@@ -22,7 +22,8 @@ definition(
|
||||
category: "SmartThings Labs",
|
||||
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",
|
||||
oauth: true
|
||||
oauth: true,
|
||||
singleInstance: true
|
||||
){
|
||||
appSetting "clientId"
|
||||
appSetting "clientSecret"
|
||||
|
||||
@@ -341,6 +341,13 @@ def eventHandler(name, value) {
|
||||
|
||||
def eventBuffer = atomicState.eventBuffer
|
||||
def epoch = now() / 1000
|
||||
|
||||
// if for some reason this code block is being run
|
||||
// but the SmartApp wasn't propery setup during install
|
||||
// we need to set initialize the eventBuffer.
|
||||
if (!atomicState.eventBuffer) {
|
||||
atomicState.eventBuffer = []
|
||||
}
|
||||
eventBuffer << [key: "$name", value: "$value", epoch: "$epoch"]
|
||||
|
||||
log.debug eventBuffer
|
||||
|
||||
@@ -13,11 +13,11 @@ definition(
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up@2x.png",
|
||||
oauth: true,
|
||||
usePreferencesForAuthorization: false
|
||||
usePreferencesForAuthorization: false,
|
||||
singleInstance: true
|
||||
) {
|
||||
appSetting "clientId"
|
||||
appSetting "clientSecret"
|
||||
appSetting "serverUrl"
|
||||
}
|
||||
|
||||
preferences {
|
||||
@@ -28,16 +28,13 @@ mappings {
|
||||
path("/receivedToken") { action: [ POST: "receivedToken", GET: "receivedToken"] }
|
||||
path("/receiveToken") { action: [ POST: "receiveToken", GET: "receiveToken"] }
|
||||
path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] }
|
||||
path("/oauth/initialize") {action: [GET: "oauthInitUrl"]}
|
||||
path("/oauth/callback") { action: [ GET: "callback" ] }
|
||||
}
|
||||
|
||||
def getSmartThingsClientId() {
|
||||
return appSettings.clientId
|
||||
}
|
||||
|
||||
def getSmartThingsClientSecret() {
|
||||
return appSettings.clientSecret
|
||||
}
|
||||
def getServerUrl() { return "https://graph.api.smartthings.com" }
|
||||
def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" }
|
||||
def buildRedirectUrl(page) { return buildActionUrl(page) }
|
||||
|
||||
def callback() {
|
||||
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.
|
||||
// Instead, go initiate the Jawbone OAuth flow.
|
||||
log.debug "Executing callback redirect to auth page"
|
||||
def stcid = getSmartThingsClientId()
|
||||
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)}")
|
||||
}
|
||||
} else {
|
||||
@@ -84,10 +80,11 @@ def authPage() {
|
||||
createAccessToken()
|
||||
}
|
||||
description = "Click to enter Jawbone Credentials"
|
||||
def redirectUrl = oauthInitUrl()
|
||||
// log.debug "RedirectURL = ${redirectUrl}"
|
||||
return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install:false) {
|
||||
section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", description:description }
|
||||
def redirectUrl = buildRedirectUrl
|
||||
log.debug "RedirectURL = ${redirectUrl}"
|
||||
def donebutton= state.JawboneAccessToken != null
|
||||
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 {
|
||||
description = "Jawbone Credentials Already Entered."
|
||||
@@ -99,17 +96,14 @@ def authPage() {
|
||||
|
||||
def oauthInitUrl() {
|
||||
log.debug "oauthInitUrl"
|
||||
def stcid = getSmartThingsClientId()
|
||||
state.oauthInitState = UUID.randomUUID().toString()
|
||||
def oauthParams = [ response_type: "code", client_id: stcid, scope: "move_read sleep_read", redirect_uri: buildRedirectUrl("receiveToken") ]
|
||||
return "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}"
|
||||
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)}")
|
||||
}
|
||||
|
||||
def receiveToken(redirectUrl = null) {
|
||||
log.debug "receiveToken"
|
||||
def stcid = getSmartThingsClientId()
|
||||
def oauthClientSecret = getSmartThingsClientSecret()
|
||||
def oauthParams = [ client_id: stcid, client_secret: oauthClientSecret, grant_type: "authorization_code", code: params.code ]
|
||||
def oauthParams = [ client_id: appSettings.clientId, client_secret: appSettings.clientSecret, grant_type: "authorization_code", code: params.code ]
|
||||
def params = [
|
||||
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("&")
|
||||
}
|
||||
|
||||
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() {
|
||||
log.debug "validateCurrentToken"
|
||||
def url = "https://jawbone.com/nudge/api/v.1.1/users/@me/refreshToken"
|
||||
def requestBody = "secret=${getSmartThingsClientSecret()}"
|
||||
def requestBody = "secret=${appSettings.clientSecret}"
|
||||
|
||||
try {
|
||||
httpPost(uri: url, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], body: requestBody) {response ->
|
||||
@@ -256,9 +242,7 @@ def validateCurrentToken() {
|
||||
if (e.statusCode == 401) { // token is expired
|
||||
log.debug "Access token is expired"
|
||||
if (state.refreshToken) { // if we have this we are okay
|
||||
def stcid = getSmartThingsClientId()
|
||||
def oauthClientSecret = getSmartThingsClientSecret()
|
||||
def oauthParams = [client_id: stcid, client_secret: oauthClientSecret, grant_type: "refresh_token", refresh_token: state.refreshToken]
|
||||
def oauthParams = [client_id: appSettings.clientId, client_secret: appSettings.clientSecret, grant_type: "refresh_token", refresh_token: state.refreshToken]
|
||||
def tokenUrl = "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}"
|
||||
def params = [
|
||||
uri: tokenUrl
|
||||
@@ -287,9 +271,10 @@ def validateCurrentToken() {
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
def hookUrl = "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback"
|
||||
def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl"
|
||||
log.debug "Callback URL: $webhook"
|
||||
log.debug "Callback URL - Webhook"
|
||||
def localServerUrl = getApiServerUrl()
|
||||
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}" ])
|
||||
}
|
||||
|
||||
@@ -327,7 +312,6 @@ def setup() {
|
||||
}
|
||||
|
||||
def installed() {
|
||||
enableCallback()
|
||||
|
||||
if (!state.accessToken) {
|
||||
log.debug "About to create access token"
|
||||
@@ -340,7 +324,6 @@ def installed() {
|
||||
}
|
||||
|
||||
def updated() {
|
||||
enableCallback()
|
||||
|
||||
if (!state.accessToken) {
|
||||
log.debug "About to create access token"
|
||||
@@ -499,4 +482,4 @@ def hookEventHandler() {
|
||||
|
||||
def html = """{"code":200,"message":"OK"}"""
|
||||
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",
|
||||
iconX2Url: "http://i.imgur.com/HU0ANBp.png",
|
||||
iconX3Url: "http://i.imgur.com/HU0ANBp.png",
|
||||
oauth: true)
|
||||
oauth: true,
|
||||
singleInstance: true)
|
||||
|
||||
|
||||
preferences {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* The timeout perid begins when the contact is closed, or motion stops, so leaving a door open won't start the timer until it's closed.
|
||||
*
|
||||
* Author: andersheie@gmail.com
|
||||
* Date: 2014-08-31
|
||||
* Date: 2015-10-30
|
||||
*/
|
||||
|
||||
definition(
|
||||
@@ -23,12 +23,18 @@ definition(
|
||||
iconX2Url: "http://upload.wikimedia.org/wikipedia/commons/6/6a/Light_bulb_icon_tips.svg")
|
||||
|
||||
preferences {
|
||||
section("Turn on when there's movement..."){
|
||||
section("Turn on when there is movement..."){
|
||||
input "motions", "capability.motionSensor", multiple: true, title: "Select motion detectors", required: false
|
||||
}
|
||||
section("Or, turn on when one of these contacts opened"){
|
||||
input "contacts", "capability.contactSensor", multiple: true, title: "Select Contacts", required: false
|
||||
}
|
||||
section("Or, turn on when any of these people come home") {
|
||||
input "peoplearrive", "capability.presenceSensor", multiple: true, required: false
|
||||
}
|
||||
section("Or, turn on when any of these people leave") {
|
||||
input "peopleleave", "capability.presenceSensor", multiple: true, required: false
|
||||
}
|
||||
section("And off after no more triggers after..."){
|
||||
input "minutes1", "number", title: "Minutes?", defaultValue: "5"
|
||||
}
|
||||
@@ -40,109 +46,232 @@ preferences {
|
||||
|
||||
def installed()
|
||||
{
|
||||
subscribe(switches, "switch", switchChange)
|
||||
subscribe(motions, "motion", motionHandler)
|
||||
subscribe(contacts, "contact", contactHandler)
|
||||
schedule("0 * * * * ?", "scheduleCheck")
|
||||
state.myState = "ready"
|
||||
log.debug "installed()"
|
||||
initialize()
|
||||
}
|
||||
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
log.debug "updated()"
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize()
|
||||
{
|
||||
log.debug "initialize()"
|
||||
// Reset to new set of Switches, just in case.
|
||||
unsubscribe()
|
||||
|
||||
state.switches = [:]
|
||||
|
||||
switches.each {
|
||||
// log.debug "ID: $it.id"
|
||||
// Set ready state for each switch
|
||||
state.switches["$it.id"] = "ready"
|
||||
}
|
||||
logStates()
|
||||
subscribe(motions, "motion", motionHandler)
|
||||
subscribe(switches, "switch", switchChange)
|
||||
subscribe(contacts, "contact", contactHandler)
|
||||
subscribe(peoplearrive, "presence.present", presencePresentHandler)
|
||||
subscribe(peopleleave, "presence.not present", presenceNotPresentHandler)
|
||||
state.lastAction = null
|
||||
schedule("0 * * * * ?", "scheduleCheck")
|
||||
|
||||
}
|
||||
|
||||
def logStates() {
|
||||
if(state.switches == null) {
|
||||
state.switches = [:]
|
||||
}
|
||||
state.switches.each {
|
||||
log.debug "State: $it.key = $it.value"
|
||||
}
|
||||
}
|
||||
|
||||
def turnSwitchOn(id) {
|
||||
for(S in switches) {
|
||||
if(S.id == id) {
|
||||
S.on()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def turnSwitchOff(id) {
|
||||
for(S in switches) {
|
||||
if(S.id == id) {
|
||||
S.off()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def initState(deviceID) {
|
||||
// Have to add this so existing apps that are neither updated nor initiated can slowly get 'reset' with states.
|
||||
// Smartthings doesn't call any specifc method when new version is published.
|
||||
|
||||
if(state.switches == null) {
|
||||
state.switches = [:]
|
||||
}
|
||||
if(state.switches[deviceID] == null) {
|
||||
state.switches[deviceID] = "ready"
|
||||
}
|
||||
|
||||
state.myState = "ready"
|
||||
log.debug "state: " + state.myState
|
||||
}
|
||||
|
||||
def switchChange(evt) {
|
||||
log.debug "SwitchChange: $evt.name: $evt.value"
|
||||
def deviceID = evt.device.id
|
||||
log.debug "SwitchChange: $evt.name: $evt.value $evt.device.id current state = " + state.switches[deviceID]
|
||||
initState(deviceID)
|
||||
|
||||
if(evt.value == "on") {
|
||||
// Slight change of Race condition between motion or contact turning the switch on,
|
||||
// versus user turning the switch on. Since we can't pass event parameters :-(, we rely
|
||||
// on the state and hope the best.
|
||||
if(state.myState == "activating") {
|
||||
// OK, probably an event from Activating something, and not the switch itself. Go to Active mode.
|
||||
state.myState = "active"
|
||||
} else if(state.myState != "active") {
|
||||
state.myState = "already on"
|
||||
}
|
||||
if(evt.value == "on") {
|
||||
if(state.switches[deviceID] == "activating") {
|
||||
// OK, probably an event from Activating something, and not the switch itself. Go to Active mode.
|
||||
log.debug "$deviceID = " + state.switches[deviceID] + " -> active"
|
||||
state.switches[deviceID] = "active"
|
||||
} else if(state.switches[deviceID] != "active") {
|
||||
log.debug "$deviceID = " + state.switches[deviceID] + " -> already on"
|
||||
state.switches[deviceID] = "already on"
|
||||
}
|
||||
} else {
|
||||
// If active and switch is turned of manually, then stop the schedule and go to ready state
|
||||
if(state.myState == "active" || state.myState == "activating") {
|
||||
unschedule()
|
||||
}
|
||||
state.myState = "ready"
|
||||
// If active and switch is turned of manually, then stop the schedule and go to ready state
|
||||
state.switches[deviceID] = "ready"
|
||||
}
|
||||
log.debug "state: " + state.myState
|
||||
logStates()
|
||||
|
||||
}
|
||||
|
||||
def contactHandler(evt) {
|
||||
log.debug "contactHandler: $evt.name: $evt.value"
|
||||
|
||||
if (evt.value == "open") {
|
||||
if(state.myState == "ready") {
|
||||
log.debug "Turning on lights by contact opening"
|
||||
switches.on()
|
||||
state.inactiveAt = null
|
||||
state.myState = "activating"
|
||||
}
|
||||
} else if (evt.value == "closed") {
|
||||
if (!state.inactiveAt && state.myState == "active" || state.myState == "activating") {
|
||||
// When contact closes, we reset the timer if not already set
|
||||
setActiveAndSchedule()
|
||||
state.switches.each { thisswitch ->
|
||||
initState(thisswitch.key)
|
||||
//log.debug "Looking for $thisswitch.key"
|
||||
if (evt.value == "open") {
|
||||
if(state.switches[thisswitch.key] == "ready") {
|
||||
log.debug "Turning on lights by contact opening: $thisswitch"
|
||||
log.debug "$thisswitch.key = " + state.switches[thisswitch.key] + " -> activating"
|
||||
state.switches[thisswitch.key] = "activating"
|
||||
turnSwitchOn(thisswitch.key)
|
||||
setActiveAndSchedule()
|
||||
|
||||
}
|
||||
} else if (evt.value == "closed") {
|
||||
if (!state.lastAction && (state.switches[thisswitch.key] == "active"
|
||||
|| state.switches[thisswitch.key] == "activating")) {
|
||||
// When contact closes, we reset the timer if not already set
|
||||
log.debug "$thisswitch.key = " + state.switches[thisswitch.key] + " -> active"
|
||||
state.switches[thisswitch.key] = "active"
|
||||
setActiveAndSchedule()
|
||||
}
|
||||
}
|
||||
}
|
||||
log.debug "state: " + state.myState
|
||||
logStates()
|
||||
}
|
||||
|
||||
def motionHandler(evt) {
|
||||
log.debug "motionHandler: $evt.name: $evt.value"
|
||||
|
||||
if (evt.value == "active") {
|
||||
if(state.myState == "ready" || state.myState == "active" || state.myState == "activating" ) {
|
||||
log.debug "turning on lights"
|
||||
switches.on()
|
||||
state.inactiveAt = null
|
||||
state.myState = "activating"
|
||||
}
|
||||
} else if (evt.value == "inactive") {
|
||||
if (!state.inactiveAt && state.myState == "active" || state.myState == "activating") {
|
||||
// When Motion ends, we reset the timer if not already set
|
||||
setActiveAndSchedule()
|
||||
def scheduleCheck() {
|
||||
log.debug "schedule check, ts = ${state.lastAction}"
|
||||
boolean resetInactive = false
|
||||
state.switches.each { thisswitch ->
|
||||
initState(thisswitch.key)
|
||||
if(state.switches[thisswitch.key] != "already on") {
|
||||
if(state.lastAction != null) {
|
||||
def elapsed = now() - state.lastAction
|
||||
log.debug "${elapsed / 1000} sec since events stopped"
|
||||
def threshold = 1000 * 60 * minutes1
|
||||
if (elapsed >= threshold) {
|
||||
if (state.switches[thisswitch.key] == "active"
|
||||
|| state.switches[thisswitch.key] == "activating") {
|
||||
state.switches[thisswitch.key] = "ready"
|
||||
log.debug "Turning off lights by switch closing: $thisswitch"
|
||||
turnSwitchOff(thisswitch.key)
|
||||
}
|
||||
resetInactive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log.debug "state: " + state.myState
|
||||
if(resetInactive) {
|
||||
state.lastAction = null
|
||||
unschedule()
|
||||
}
|
||||
logStates()
|
||||
}
|
||||
|
||||
|
||||
/**********************************************************************/
|
||||
|
||||
def motionHandler(evt) {
|
||||
log.debug "motionHandler: $evt.name: $evt.value (current state: " + state.myState + ")"
|
||||
|
||||
|
||||
state.switches.each { thisswitch ->
|
||||
initState(thisswitch.key)
|
||||
if (evt.value == "active") {
|
||||
if(state.switches[thisswitch.key] == "ready"
|
||||
|| state.switches[thisswitch.key] == "active"
|
||||
|| state.switches[thisswitch.key] == "activating" ) {
|
||||
log.debug "$thisswitch.key = " + state.switches[thisswitch.key] + " -> activating"
|
||||
state.switches[thisswitch.key] = "activating"
|
||||
turnSwitchOn(thisswitch.key)
|
||||
setActiveAndSchedule()
|
||||
}
|
||||
} else if (evt.value == "inactive") {
|
||||
if (state.switches[thisswitch.key] == "active" || state.switches[thisswitch.key] == "activating") {
|
||||
// When Motion ends, we reset the timer if not already set
|
||||
log.debug "$thisswitch.key = " + state.switches[thisswitch.key] + " -> active"
|
||||
state.switches[thisswitch.key] = "active"
|
||||
setActiveAndSchedule()
|
||||
}
|
||||
}
|
||||
}
|
||||
logStates()
|
||||
}
|
||||
|
||||
def presencePresentHandler(evt) {
|
||||
log.debug "presence: $evt.linkText is now $evt.value"
|
||||
|
||||
state.switches.each { thisswitch ->
|
||||
initState(thisswitch.key)
|
||||
if (evt.value == "present") {
|
||||
if(state.switches[thisswitch.key] == "ready"
|
||||
|| state.switches[thisswitch.key] == "active"
|
||||
|| state.switches[thisswitch.key] == "activating" ) {
|
||||
log.debug "Presence turning on switch $thisswitch"
|
||||
state.switches[thisswitch.key] = "active"
|
||||
turnSwitchOn(thisswitch.key)
|
||||
// We don't wait until the person leave, but instead start timer immediately.
|
||||
setActiveAndSchedule()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
logStates()
|
||||
}
|
||||
|
||||
def presenceNotPresentHandler(evt) {
|
||||
log.debug "presence: $evt.linkText is now $evt.value"
|
||||
|
||||
state.switches.each { thisswitch ->
|
||||
initState(thisswitch.key)
|
||||
if (evt.value == "not present") {
|
||||
if(state.switches[thisswitch.key] == "ready"
|
||||
|| state.switches[thisswitch.key] == "active"
|
||||
|| state.switches[thisswitch.key] == "activating" ) {
|
||||
log.debug "No Presence turning on lights $thisswitch"
|
||||
state.switches[thisswitch.key] = "active"
|
||||
turnSwitchOn(thisswitch.key)
|
||||
// We don't wait until the person arrive back, but instead start timer immediately.
|
||||
setActiveAndSchedule()
|
||||
}
|
||||
}
|
||||
}
|
||||
logStates()
|
||||
}
|
||||
|
||||
def setActiveAndSchedule() {
|
||||
unschedule()
|
||||
state.myState = "active"
|
||||
state.inactiveAt = now()
|
||||
state.lastAction = now()
|
||||
schedule("0 * * * * ?", "scheduleCheck")
|
||||
log.debug "Scheduled new timer"
|
||||
}
|
||||
|
||||
def scheduleCheck() {
|
||||
log.debug "schedule check, ts = ${state.inactiveAt}"
|
||||
if(state.myState != "already on") {
|
||||
if(state.inactiveAt != null) {
|
||||
def elapsed = now() - state.inactiveAt
|
||||
log.debug "${elapsed / 1000} sec since motion stopped"
|
||||
def threshold = 1000 * 60 * minutes1
|
||||
if (elapsed >= threshold) {
|
||||
if (state.myState == "active") {
|
||||
log.debug "turning off lights"
|
||||
switches.off()
|
||||
}
|
||||
state.inactiveAt = null
|
||||
state.myState = "ready"
|
||||
}
|
||||
}
|
||||
}
|
||||
log.debug "state: " + state.myState
|
||||
}
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
/**
|
||||
* Hello Home Cube
|
||||
*
|
||||
* Copyright 2015 skp19
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
/************
|
||||
* Metadata *
|
||||
************/
|
||||
definition(
|
||||
name: "Hello Home Cube",
|
||||
namespace: "skp19",
|
||||
author: "skp19",
|
||||
description: "Run a Hello Home action by rotating a cube containing a SmartSense Multi",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld@2x.png"
|
||||
)
|
||||
|
||||
import groovy.json.JsonSlurper
|
||||
|
||||
/**********
|
||||
* Setup *
|
||||
**********/
|
||||
preferences {
|
||||
page(name: "mainPage", title: "", nextPage: "scenesPage", uninstall: true) {
|
||||
section("Use the orientation of this cube") {
|
||||
input "cube", "capability.threeAxis", required: false, title: "SmartSense Multi sensor"
|
||||
}
|
||||
section([title: " ", mobileOnly:true]) {
|
||||
label title: "Assign a name", required: false
|
||||
mode title: "Set for specific mode(s)", required: false
|
||||
}
|
||||
}
|
||||
page(name: "scenesPage", title: "Scenes", install: true, uninstall: true)
|
||||
}
|
||||
|
||||
|
||||
def scenesPage() {
|
||||
log.debug "scenesPage()"
|
||||
def sceneId = getOrientation()
|
||||
dynamicPage(name:"scenesPage") {
|
||||
def phrases = location.helloHome?.getPhrases()*.label
|
||||
section {
|
||||
phrases.sort()
|
||||
input name: "homeAction1", type: "enum", title: "${1}. ${sceneName(1)}${sceneId==1 ? ' (current)' : ''}", required: false, options: phrases
|
||||
input name: "homeAction2", type: "enum", title: "${2}. ${sceneName(2)}${sceneId==2 ? ' (current)' : ''}", required: false, options: phrases
|
||||
input name: "homeAction3", type: "enum", title: "${3}. ${sceneName(3)}${sceneId==3 ? ' (current)' : ''}", required: false, options: phrases
|
||||
input name: "homeAction4", type: "enum", title: "${4}. ${sceneName(4)}${sceneId==4 ? ' (current)' : ''}", required: false, options: phrases
|
||||
input name: "homeAction5", type: "enum", title: "${5}. ${sceneName(5)}${sceneId==5 ? ' (current)' : ''}", required: false, options: phrases
|
||||
input name: "homeAction6", type: "enum", title: "${6}. ${sceneName(6)}${sceneId==6 ? ' (current)' : ''}", required: false, options: phrases
|
||||
}
|
||||
section {
|
||||
href "scenesPage", title: "Refresh", description: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*************************
|
||||
* Installation & update *
|
||||
*************************/
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
subscribe cube, "threeAxis", positionHandler
|
||||
}
|
||||
|
||||
|
||||
/******************
|
||||
* Event handlers *
|
||||
******************/
|
||||
def positionHandler(evt) {
|
||||
|
||||
def sceneId = getOrientation(evt.xyzValue)
|
||||
log.trace "orientation: $sceneId"
|
||||
|
||||
if (sceneId != state.lastActiveSceneId) {
|
||||
runHomeAction(sceneId)
|
||||
}
|
||||
else {
|
||||
log.trace "No status change"
|
||||
}
|
||||
state.lastActiveSceneId = sceneId
|
||||
}
|
||||
|
||||
|
||||
/******************
|
||||
* Helper methods *
|
||||
******************/
|
||||
private Boolean sceneIsDefined(sceneId) {
|
||||
def tgt = "onoff_${sceneId}".toString()
|
||||
settings.find{it.key.startsWith(tgt)} != null
|
||||
}
|
||||
|
||||
private updateSetting(name, value) {
|
||||
app.updateSetting(name, value)
|
||||
settings[name] = value
|
||||
}
|
||||
|
||||
private runHomeAction(sceneId) {
|
||||
log.trace "runHomeAction($sceneId)"
|
||||
|
||||
//RUN HELLO HOME ACTION
|
||||
def homeAction
|
||||
if (sceneId == 1) {
|
||||
homeAction = homeAction1
|
||||
}
|
||||
if (sceneId == 2) {
|
||||
homeAction = homeAction2
|
||||
}
|
||||
if (sceneId == 3) {
|
||||
homeAction = homeAction3
|
||||
}
|
||||
if (sceneId == 4) {
|
||||
homeAction = homeAction4
|
||||
}
|
||||
if (sceneId == 5) {
|
||||
homeAction = homeAction5
|
||||
}
|
||||
if (sceneId == 6) {
|
||||
homeAction = homeAction6
|
||||
}
|
||||
|
||||
if (homeAction) {
|
||||
location.helloHome.execute(homeAction)
|
||||
log.trace "Running Home Action: $homeAction"
|
||||
}
|
||||
else {
|
||||
log.trace "No Home Action Defined for Current State"
|
||||
}
|
||||
}
|
||||
|
||||
private getOrientation(xyz=null) {
|
||||
final threshold = 250
|
||||
|
||||
def value = xyz ?: cube.currentValue("threeAxis")
|
||||
|
||||
def x = Math.abs(value.x) > threshold ? (value.x > 0 ? 1 : -1) : 0
|
||||
def y = Math.abs(value.y) > threshold ? (value.y > 0 ? 1 : -1) : 0
|
||||
def z = Math.abs(value.z) > threshold ? (value.z > 0 ? 1 : -1) : 0
|
||||
|
||||
def orientation = 0
|
||||
if (z > 0) {
|
||||
if (x == 0 && y == 0) {
|
||||
orientation = 1
|
||||
}
|
||||
}
|
||||
else if (z < 0) {
|
||||
if (x == 0 && y == 0) {
|
||||
orientation = 2
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (x > 0) {
|
||||
if (y == 0) {
|
||||
orientation = 3
|
||||
}
|
||||
}
|
||||
else if (x < 0) {
|
||||
if (y == 0) {
|
||||
orientation = 4
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (y > 0) {
|
||||
orientation = 5
|
||||
}
|
||||
else if (y < 0) {
|
||||
orientation = 6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
orientation
|
||||
}
|
||||
|
||||
private sceneName(num) {
|
||||
final names = ["UNDEFINED","One","Two","Three","Four","Five","Six"]
|
||||
settings."sceneName${num}" ?: "Scene ${names[num]}"
|
||||
}
|
||||
@@ -14,8 +14,8 @@
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Smart Lock / Unlock",
|
||||
namespace: "",
|
||||
name: "Smart Auto Lock / Unlock",
|
||||
namespace: "smart-auto-lock-unlock",
|
||||
author: "Arnaud",
|
||||
description: "Automatically locks door X minutes after being closed and keeps door unlocked if door is open.",
|
||||
category: "Safety & Security",
|
||||
@@ -21,7 +21,8 @@
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
||||
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png"
|
||||
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
||||
singleInstance: true
|
||||
)
|
||||
|
||||
preferences {
|
||||
|
||||
@@ -26,7 +26,8 @@ definition(
|
||||
description: "Connect your Ecobee thermostat to SmartThings.",
|
||||
category: "SmartThings Labs",
|
||||
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 "serverUrl"
|
||||
|
||||
@@ -23,7 +23,8 @@ definition(
|
||||
description: "Connect and take pictures using your Foscam camera from inside the Smartthings app.",
|
||||
category: "SmartThings Internal",
|
||||
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 {
|
||||
|
||||
@@ -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.",
|
||||
category: "SmartThings Labs",
|
||||
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 {
|
||||
@@ -68,6 +69,7 @@ def bridgeDiscovery(params=[:])
|
||||
log.trace "Cleaning old bridges memory"
|
||||
state.bridges = [:]
|
||||
state.bridgeRefreshCount = 0
|
||||
app.updateSetting("selectedHue", "")
|
||||
}
|
||||
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
@@ -131,17 +133,24 @@ def bulbDiscovery() {
|
||||
state.bulbRefreshCount = bulbRefreshCount + 1
|
||||
def refreshInterval = 3
|
||||
state.inBulbDiscovery = true
|
||||
def bridge = null
|
||||
if (selectedHue) {
|
||||
bridge = getChildDevice(selectedHue)
|
||||
subscribe(bridge, "bulbList", bulbListData)
|
||||
}
|
||||
state.bridgeRefreshCount = 0
|
||||
def options = bulbsDiscovered() ?: []
|
||||
def numFound = options.size() ?: 0
|
||||
|
||||
if((bulbRefreshCount % 3) == 0) {
|
||||
def bulboptions = bulbsDiscovered() ?: [:]
|
||||
def numFound = bulboptions.size() ?: 0
|
||||
if (numFound == 0)
|
||||
app.updateSetting("selectedBulbs", "")
|
||||
|
||||
if((bulbRefreshCount % 5) == 0) {
|
||||
discoverHueBulbs()
|
||||
}
|
||||
|
||||
return dynamicPage(name:"bulbDiscovery", title:"Bulb Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
||||
section("Please wait while we discover your Hue Bulbs. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||
input "selectedBulbs", "enum", required:false, title:"Select Hue Bulbs (${numFound} found)", multiple:true, options:options
|
||||
input "selectedBulbs", "enum", required:false, title:"Select Hue Bulbs (${numFound} found)", multiple:true, options:bulboptions
|
||||
}
|
||||
section {
|
||||
def title = getBridgeIP() ? "Hue bridge (${getBridgeIP()})" : "Find bridges"
|
||||
@@ -223,10 +232,14 @@ Map bulbsDiscovered() {
|
||||
bulbmap["${key}"] = value
|
||||
}
|
||||
}
|
||||
bulbmap
|
||||
return bulbmap
|
||||
}
|
||||
|
||||
def getHueBulbs() {
|
||||
def bulbListData(evt) {
|
||||
state.bulbs = evt.jsonData
|
||||
}
|
||||
|
||||
Map getHueBulbs() {
|
||||
state.bulbs = state.bulbs ?: [:]
|
||||
}
|
||||
|
||||
@@ -252,7 +265,10 @@ def updated() {
|
||||
|
||||
def initialize() {
|
||||
log.debug "Initializing"
|
||||
unsubscribe(bridge)
|
||||
state.inBulbDiscovery = false
|
||||
state.bridgeRefreshCount = 0
|
||||
state.bulbRefreshCount = 0
|
||||
if (selectedHue) {
|
||||
addBridge()
|
||||
addBulbs()
|
||||
@@ -276,9 +292,8 @@ def uninstalled(){
|
||||
// Handles events to add new bulbs
|
||||
def bulbListHandler(hub, data = "") {
|
||||
def msg = "Bulbs list not processed. Only while in settings menu."
|
||||
log.trace "Here: $hub, $data"
|
||||
if (state.inBulbDiscovery) {
|
||||
def bulbs = [:]
|
||||
def bulbs = [:]
|
||||
if (state.inBulbDiscovery) {
|
||||
def logg = ""
|
||||
log.trace "Adding bulbs to state..."
|
||||
state.bridgeProcessedLightList = true
|
||||
@@ -287,25 +302,32 @@ def bulbListHandler(hub, data = "") {
|
||||
if (v instanceof Map)
|
||||
bulbs[k] = [id: k, name: v.name, type: v.type, hub:hub]
|
||||
}
|
||||
state.bulbs = bulbs
|
||||
msg = "${bulbs.size()} bulbs found. $state.bulbs"
|
||||
}
|
||||
}
|
||||
def bridge = null
|
||||
if (selectedHue)
|
||||
bridge = getChildDevice(selectedHue)
|
||||
bridge.sendEvent(name: "bulbList", value: hub, data: bulbs, isStateChange: true, displayed: false)
|
||||
msg = "${bulbs.size()} bulbs found. ${bulbs}"
|
||||
return msg
|
||||
}
|
||||
|
||||
def addBulbs() {
|
||||
def bulbs = getHueBulbs()
|
||||
selectedBulbs.each { dni ->
|
||||
selectedBulbs?.each { dni ->
|
||||
def d = getChildDevice(dni)
|
||||
if(!d) {
|
||||
def newHueBulb
|
||||
if (bulbs instanceof java.util.Map) {
|
||||
newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
|
||||
if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light")) {
|
||||
d = addChildDevice("smartthings", "Hue Lux Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
||||
} else {
|
||||
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
||||
}
|
||||
if (newHueBulb != null) {
|
||||
if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light") ) {
|
||||
d = addChildDevice("smartthings", "Hue Lux 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 {
|
||||
//backwards compatable
|
||||
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
|
||||
@@ -413,8 +435,11 @@ def locationHandler(evt) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
networkAddress = d.latestState('networkAddress').stringValue
|
||||
} else {
|
||||
if (d.getDeviceDataByName("networkAddress"))
|
||||
networkAddress = d.getDeviceDataByName("networkAddress")
|
||||
else
|
||||
networkAddress = d.latestState('networkAddress').stringValue
|
||||
log.trace "Host: $host - $networkAddress"
|
||||
if(host != networkAddress) {
|
||||
log.debug "Device's port or ip changed for device $d..."
|
||||
@@ -422,7 +447,8 @@ def locationHandler(evt) {
|
||||
dstate.port = port
|
||||
dstate.name = "Philips hue ($ip)"
|
||||
d.sendEvent(name:"networkAddress", value: host)
|
||||
}
|
||||
d.updateDataValue("networkAddress", host)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -583,23 +609,22 @@ def parse(childDevice, description) {
|
||||
}
|
||||
}
|
||||
|
||||
def on(childDevice, transition_deprecated = 0) {
|
||||
def on(childDevice) {
|
||||
log.debug "Executing 'on'"
|
||||
def percent = childDevice.device?.currentValue("level") as Integer
|
||||
def level = Math.min(Math.round(percent * 255 / 100), 255)
|
||||
put("lights/${getId(childDevice)}/state", [bri: level, on: true])
|
||||
return "level: $percent"
|
||||
put("lights/${getId(childDevice)}/state", [on: true])
|
||||
return "Bulb is On"
|
||||
}
|
||||
|
||||
def off(childDevice, transition_deprecated = 0) {
|
||||
def off(childDevice) {
|
||||
log.debug "Executing 'off'"
|
||||
put("lights/${getId(childDevice)}/state", [on: false])
|
||||
return "level: 0"
|
||||
return "Bulb is Off"
|
||||
}
|
||||
|
||||
def setLevel(childDevice, percent) {
|
||||
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])
|
||||
}
|
||||
|
||||
@@ -615,7 +640,7 @@ def setHue(childDevice, percent) {
|
||||
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)'"
|
||||
def hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
|
||||
def sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255)
|
||||
@@ -624,7 +649,7 @@ def setColor(childDevice, huesettings, alert_deprecated = "", transition_depreca
|
||||
|
||||
def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition]
|
||||
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
|
||||
}
|
||||
|
||||
@@ -701,6 +726,9 @@ private getBridgeIP() {
|
||||
if (host == null || host == "") {
|
||||
def serialNumber = selectedHue
|
||||
def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value
|
||||
if (!bridge) {
|
||||
bridge = getHueBridges().find { it?.value?.mac?.equalsIgnoreCase(serialNumber) }?.value
|
||||
}
|
||||
if (bridge?.ip && bridge?.port) {
|
||||
if (bridge?.ip.contains("."))
|
||||
host = "${bridge?.ip}:${bridge?.port}"
|
||||
|
||||
@@ -98,6 +98,15 @@ def installed() {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ definition(
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/life360.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 "clientSecret"
|
||||
|
||||
@@ -13,7 +13,8 @@ definition(
|
||||
iconUrl: "https://cloud.lifx.com/images/lifx.png",
|
||||
iconX2Url: "https://cloud.lifx.com/images/lifx.png",
|
||||
iconX3Url: "https://cloud.lifx.com/images/lifx.png",
|
||||
oauth: true) {
|
||||
oauth: true,
|
||||
singleInstance: true) {
|
||||
appSetting "clientId"
|
||||
appSetting "clientSecret"
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
* locks | lock | lock, unlock | locked, unlocked
|
||||
* ---------------------+-------------------+-----------------------------+------------------------------------
|
||||
*/
|
||||
|
||||
|
||||
definition(
|
||||
name: "Logitech Harmony (Connect)",
|
||||
namespace: "smartthings",
|
||||
@@ -43,11 +43,11 @@ definition(
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.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 "clientSecret"
|
||||
appSetting "callbackUrl"
|
||||
}
|
||||
|
||||
preferences(oauthPage: "deviceAuthorization") {
|
||||
@@ -89,16 +89,18 @@ mappings {
|
||||
}
|
||||
|
||||
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 description = null
|
||||
def description = null
|
||||
if (!state.HarmonyAccessToken) {
|
||||
if (!state.accessToken) {
|
||||
log.debug "About to create access token"
|
||||
createAccessToken()
|
||||
}
|
||||
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) {
|
||||
section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description }
|
||||
}
|
||||
@@ -110,7 +112,7 @@ def authPage() {
|
||||
|
||||
def huboptions = state.HarmonyHubs ?: []
|
||||
def actoptions = state.HarmonyActivities ?: []
|
||||
|
||||
|
||||
def numFoundHub = huboptions.size() ?: 0
|
||||
def numFoundAct = actoptions.size() ?: 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.") {
|
||||
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") {
|
||||
input "selectedactivities", "enum", required:false, title:"Select Harmony Activities (${numFoundAct} found)", multiple:true, options:actoptions
|
||||
}
|
||||
}
|
||||
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 {
|
||||
log.warn "No authQueryString"
|
||||
}
|
||||
|
||||
|
||||
if (state.HarmonyAccessToken) {
|
||||
log.debug "Access token already exists"
|
||||
discovery()
|
||||
@@ -163,8 +166,8 @@ def callback() {
|
||||
|
||||
def init() {
|
||||
log.debug "Requesting Code"
|
||||
def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${appSettings.callbackUrl}" ]
|
||||
redirect(location: "https://home.myharmony.com/oauth2/authorize?${toQueryString(oauthParams)}")
|
||||
def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${callbackUrl}" ]
|
||||
redirect(location: "https://home.myharmony.com/oauth2/authorize?${toQueryString(oauthParams)}")
|
||||
}
|
||||
|
||||
def receiveToken(redirectUrl = null) {
|
||||
@@ -174,7 +177,7 @@ def receiveToken(redirectUrl = null) {
|
||||
uri: "https://home.myharmony.com/oauth2/token?${toQueryString(oauthParams)}",
|
||||
]
|
||||
try {
|
||||
httpPost(params) { response ->
|
||||
httpPost(params) { response ->
|
||||
state.HarmonyAccessToken = response.data.access_token
|
||||
}
|
||||
} catch (java.util.concurrent.TimeoutException e) {
|
||||
@@ -221,7 +224,7 @@ def connectionStatus(message, redirectUrl = null) {
|
||||
<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
|
||||
"""
|
||||
}
|
||||
|
||||
|
||||
def html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -302,30 +305,28 @@ def buildRedirectUrl(page) {
|
||||
}
|
||||
|
||||
def installed() {
|
||||
enableCallback()
|
||||
if (!state.accessToken) {
|
||||
log.debug "About to create access token"
|
||||
createAccessToken()
|
||||
} else {
|
||||
} else {
|
||||
initialize()
|
||||
}
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
unschedule()
|
||||
enableCallback()
|
||||
unschedule()
|
||||
if (!state.accessToken) {
|
||||
log.debug "About to create access token"
|
||||
createAccessToken()
|
||||
} else {
|
||||
} else {
|
||||
initialize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def uninstalled() {
|
||||
if (state.HarmonyAccessToken) {
|
||||
try {
|
||||
try {
|
||||
state.HarmonyAccessToken = ""
|
||||
log.debug "Success disconnecting Harmony from SmartThings"
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
@@ -339,7 +340,7 @@ def initialize() {
|
||||
if (selectedhubs || selectedactivities) {
|
||||
addDevice()
|
||||
runEvery5Minutes("discovery")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getHarmonydevices() {
|
||||
@@ -359,20 +360,20 @@ Map discoverDevices() {
|
||||
def hubname = getHubName(it.key)
|
||||
def hubvalue = "${hubname}"
|
||||
hubs["harmony-${hubkey}"] = hubvalue
|
||||
it.value.response.data.activities.each {
|
||||
it.value.response.data.activities.each {
|
||||
def value = "${it.value.name}"
|
||||
def key = "harmony-${hubkey}-${it.key}"
|
||||
activities["${key}"] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
state.HarmonyHubs = hubs
|
||||
state.HarmonyActivities = activities
|
||||
}
|
||||
state.HarmonyActivities = activities
|
||||
}
|
||||
}
|
||||
|
||||
//CHILD DEVICE METHODS
|
||||
def discovery() {
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}"
|
||||
try {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
@@ -384,11 +385,11 @@ def discovery() {
|
||||
poll()
|
||||
} else {
|
||||
log.debug "Error: $response.status"
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
if (e.statusCode == 401) { // token is expired
|
||||
state.remove("HarmonyAccessToken")
|
||||
state.remove("HarmonyAccessToken")
|
||||
log.warn "Harmony Access token has expired"
|
||||
}
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
@@ -396,12 +397,12 @@ def discovery() {
|
||||
state.resethub = true
|
||||
} catch (e) {
|
||||
log.warn "Hostname in certificate didn't match. Please try again later."
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
def addDevice() {
|
||||
log.trace "Adding Hubs"
|
||||
log.trace "Adding Hubs"
|
||||
selectedhubs.each { dni ->
|
||||
def d = getChildDevice(dni)
|
||||
if(!d) {
|
||||
@@ -412,8 +413,8 @@ def addDevice() {
|
||||
} else {
|
||||
log.trace "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
log.trace "Adding Activities"
|
||||
}
|
||||
log.trace "Adding Activities"
|
||||
selectedactivities.each { dni ->
|
||||
def d = getChildDevice(dni)
|
||||
if(!d) {
|
||||
@@ -424,7 +425,7 @@ def addDevice() {
|
||||
} else {
|
||||
log.trace "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def activity(dni,mode) {
|
||||
@@ -432,26 +433,26 @@ def activity(dni,mode) {
|
||||
def msg = "Command failed"
|
||||
def url = ''
|
||||
if (dni == "all") {
|
||||
url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(Params)}"
|
||||
} else {
|
||||
url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(Params)}"
|
||||
} else {
|
||||
def aux = dni.split('-')
|
||||
def hubId = aux[1]
|
||||
if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){
|
||||
url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(Params)}"
|
||||
if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){
|
||||
url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(Params)}"
|
||||
} else {
|
||||
def activityId = aux[2]
|
||||
url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/${activityId}/${mode}?${toQueryString(Params)}"
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
httpPostJson(uri: url) { response ->
|
||||
httpPostJson(uri: url) { response ->
|
||||
if (response.data.code == 200 || dni == "all") {
|
||||
msg = "Command sent succesfully"
|
||||
state.aux = 0
|
||||
state.aux = 0
|
||||
} else {
|
||||
msg = "Command failed. Error: $response.data.code"
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException ex) {
|
||||
log.error ex
|
||||
if (state.aux == 0) {
|
||||
@@ -459,7 +460,7 @@ def activity(dni,mode) {
|
||||
activity(dni,mode)
|
||||
} else {
|
||||
msg = ex
|
||||
state.aux = 0
|
||||
state.aux = 0
|
||||
}
|
||||
} catch(Exception ex) {
|
||||
msg = ex
|
||||
@@ -472,10 +473,10 @@ def poll() {
|
||||
// GET THE LIST OF ACTIVITIES
|
||||
if (state.HarmonyAccessToken) {
|
||||
getActivityList()
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def url = "https://home.myharmony.com/cloudapi/state?${toQueryString(Params)}"
|
||||
try {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
def map = [:]
|
||||
response.data.hubs.each {
|
||||
if (it.value.message == "OK") {
|
||||
@@ -488,20 +489,20 @@ def poll() {
|
||||
def currentActivity = getActivityName(it.value.response.data.currentAvActivity,it.key)
|
||||
hub.sendEvent(name: "currentActivity", value: currentActivity, descriptionText: "Current activity is ${currentActivity}", display: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.trace it.value.message
|
||||
}
|
||||
}
|
||||
}
|
||||
def activities = getChildDevices()
|
||||
def activities = getChildDevices()
|
||||
def activitynotrunning = true
|
||||
activities.each { activity ->
|
||||
def act = activity.deviceNetworkId.split('-')
|
||||
def act = activity.deviceNetworkId.split('-')
|
||||
if (act.size() > 2) {
|
||||
def aux = map.find { it.key == act[1] }
|
||||
if (aux) {
|
||||
def aux2 = aux.value.split(',')
|
||||
def childDevice = getChildDevice(activity.deviceNetworkId)
|
||||
def childDevice = getChildDevice(activity.deviceNetworkId)
|
||||
if ((act[2] == aux2[0]) && (aux2[1] == "1" || aux2[1] == "2")) {
|
||||
childDevice?.sendEvent(name: "switch", value: "on")
|
||||
if (aux2[1] == "1")
|
||||
@@ -511,30 +512,30 @@ def poll() {
|
||||
if (aux2[1] == "3")
|
||||
runIn(5, "poll", [overwrite: true])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "Poll completed $map - $state.hubs"
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
if (e.statusCode == 401) { // token is expired
|
||||
state.remove("HarmonyAccessToken")
|
||||
state.remove("HarmonyAccessToken")
|
||||
return "Harmony Access token has expired"
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def getActivityList() {
|
||||
// GET ACTIVITY'S NAME
|
||||
if (state.HarmonyAccessToken) {
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}"
|
||||
try {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
response.data.hubs.each {
|
||||
def hub = getChildDevice("harmony-${it.key}")
|
||||
if (hub) {
|
||||
@@ -547,10 +548,10 @@ def getActivityList() {
|
||||
}
|
||||
activities += [id: "off", name: "Activity OFF", type: "0"]
|
||||
log.trace activities
|
||||
}
|
||||
hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", display: false)
|
||||
}
|
||||
hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", display: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
log.trace e
|
||||
@@ -559,7 +560,7 @@ def getActivityList() {
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
return activity
|
||||
}
|
||||
|
||||
@@ -567,16 +568,16 @@ def getActivityName(activity,hubId) {
|
||||
// GET ACTIVITY'S NAME
|
||||
def actname = activity
|
||||
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)}"
|
||||
try {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
actname = response.data.data.activities[activity].name
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
return actname
|
||||
}
|
||||
|
||||
@@ -584,19 +585,19 @@ def getActivityId(activity,hubId) {
|
||||
// GET ACTIVITY'S NAME
|
||||
def actid = activity
|
||||
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)}"
|
||||
try {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
response.data.data.activities.each {
|
||||
if (it.value.name == activity)
|
||||
actid = it.key
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
return actid
|
||||
}
|
||||
|
||||
@@ -604,16 +605,16 @@ def getHubName(hubId) {
|
||||
// GET HUB'S NAME
|
||||
def hubname = hubId
|
||||
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)}"
|
||||
try {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
hubname = response.data.data.name
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return hubname
|
||||
}
|
||||
|
||||
@@ -624,8 +625,8 @@ def sendNotification(msg) {
|
||||
def hookEventHandler() {
|
||||
// log.debug "In hookEventHandler method."
|
||||
log.debug "request = ${request}"
|
||||
|
||||
def json = request.JSON
|
||||
|
||||
def json = request.JSON
|
||||
|
||||
def html = """{"code":200,"message":"OK"}"""
|
||||
render contentType: 'application/json', data: html
|
||||
@@ -659,12 +660,16 @@ def updateDevice() {
|
||||
} else {
|
||||
def device = allDevices.find { it.id == params.id }
|
||||
if (device) {
|
||||
if (arguments) {
|
||||
device."$command"(*arguments)
|
||||
} else {
|
||||
device."$command"()
|
||||
if (device.hasCommand("$command")) {
|
||||
if (arguments) {
|
||||
device."$command"(*arguments)
|
||||
} else {
|
||||
device."$command"()
|
||||
}
|
||||
render status: 204, data: "{}"
|
||||
} else {
|
||||
render status: 404, data: '{"msg": "Command not supported by this Device"}'
|
||||
}
|
||||
render status: 204, data: "{}"
|
||||
} else {
|
||||
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.",
|
||||
category: "SmartThings Labs",
|
||||
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 {
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Send HAM Bridge Command When…",
|
||||
namespace: "soletc.com",
|
||||
name: "Send HAM Bridge Command When",
|
||||
namespace: "smartthings",
|
||||
author: "Scottin Pollock",
|
||||
description: "Sends a command to your HAM Bridge server when SmartThings are activated.",
|
||||
category: "Convenience",
|
||||
@@ -25,7 +25,6 @@ definition(
|
||||
iconX2Url: "http://solutionsetcetera.com/stuff/STIcons/HB@2x.png"
|
||||
)
|
||||
|
||||
|
||||
preferences {
|
||||
section("Choose one or more, when..."){
|
||||
input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
|
||||
|
||||
@@ -23,7 +23,8 @@ definition(
|
||||
description: "Integrate your Tesla car with SmartThings.",
|
||||
category: "SmartThings Labs",
|
||||
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 {
|
||||
|
||||
@@ -22,7 +22,8 @@ definition(
|
||||
description: "Allows you to integrate your WeMo Switch and Wemo Motion sensor with SmartThings.",
|
||||
category: "SmartThings Labs",
|
||||
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 {
|
||||
@@ -61,10 +62,7 @@ def firstPage()
|
||||
|
||||
log.debug "REFRESH COUNT :: ${refreshCount}"
|
||||
|
||||
if(!state.subscribe) {
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
state.subscribe = true
|
||||
}
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
|
||||
//ssdp request every 25 seconds
|
||||
if((refreshCount % 5) == 0) {
|
||||
@@ -168,21 +166,30 @@ def getWemoLightSwitches()
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
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() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
initialize()
|
||||
}
|
||||
|
||||
runIn(5, "subscribeToDevices") //subscribe again to new/old devices wait 5 seconds
|
||||
runIn(10, "refreshDevices") //refresh devices again, delayed by 10 seconds
|
||||
def initialize() {
|
||||
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() {
|
||||
@@ -192,8 +199,7 @@ def resubscribe() {
|
||||
|
||||
def refresh() {
|
||||
log.debug "refresh() called"
|
||||
//reschedule the refreshes
|
||||
runIn(1740, "refresh", [overwrite: false])
|
||||
doDeviceSync()
|
||||
refreshDevices()
|
||||
}
|
||||
|
||||
@@ -236,7 +242,8 @@ def addSwitches() {
|
||||
"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}"
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $dni already exists"
|
||||
@@ -266,8 +273,9 @@ def addMotions() {
|
||||
"port": selectedMotion.value.port
|
||||
]
|
||||
])
|
||||
|
||||
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
|
||||
def ipvalue = convertHexToIP(selectedMotion.value.ip)
|
||||
d.sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
|
||||
log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}"
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
@@ -296,7 +304,8 @@ def addLightSwitches() {
|
||||
"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"
|
||||
} else {
|
||||
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 description = evt.description
|
||||
def hub = evt?.hubId
|
||||
@@ -333,53 +321,32 @@ def locationHandler(evt) {
|
||||
log.debug parsedEvent
|
||||
|
||||
if (parsedEvent?.ssdpTerm?.contains("Belkin:device:controllee") || parsedEvent?.ssdpTerm?.contains("Belkin:device:insight")) {
|
||||
|
||||
def switches = getWemoSwitches()
|
||||
|
||||
if (!(switches."${parsedEvent.ssdpUSN.toString()}"))
|
||||
{ //if it doesn't already exist
|
||||
if (!(switches."${parsedEvent.ssdpUSN.toString()}")) {
|
||||
//if it doesn't already exist
|
||||
switches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
|
||||
}
|
||||
else
|
||||
{ // just update the values
|
||||
|
||||
} else {
|
||||
log.debug "Device was already found in state..."
|
||||
|
||||
def d = switches."${parsedEvent.ssdpUSN.toString()}"
|
||||
boolean deviceChangedValues = false
|
||||
|
||||
log.debug "$d.ip <==> $parsedEvent.ip"
|
||||
if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) {
|
||||
d.ip = parsedEvent.ip
|
||||
d.port = parsedEvent.port
|
||||
deviceChangedValues = true
|
||||
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")) {
|
||||
|
||||
def motions = getWemoMotions()
|
||||
|
||||
if (!(motions."${parsedEvent.ssdpUSN.toString()}"))
|
||||
{ //if it doesn't already exist
|
||||
if (!(motions."${parsedEvent.ssdpUSN.toString()}")) {
|
||||
//if it doesn't already exist
|
||||
motions << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
|
||||
}
|
||||
else
|
||||
{ // just update the values
|
||||
|
||||
} else { // just update the values
|
||||
log.debug "Device was already found in state..."
|
||||
|
||||
def d = motions."${parsedEvent.ssdpUSN.toString()}"
|
||||
@@ -412,10 +379,7 @@ def locationHandler(evt) {
|
||||
if (!(lightSwitches."${parsedEvent.ssdpUSN.toString()}"))
|
||||
{ //if it doesn't already exist
|
||||
lightSwitches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent]
|
||||
}
|
||||
else
|
||||
{ // just update the values
|
||||
|
||||
} else {
|
||||
log.debug "Device was already found in state..."
|
||||
|
||||
def d = lightSwitches."${parsedEvent.ssdpUSN.toString()}"
|
||||
@@ -426,21 +390,11 @@ def locationHandler(evt) {
|
||||
d.port = parsedEvent.port
|
||||
deviceChangedValues = true
|
||||
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) {
|
||||
String headerString = new String(parsedEvent.headers.decodeBase64())?.toLowerCase()
|
||||
@@ -580,73 +534,30 @@ private def parseDiscoveryMessage(String description) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
device
|
||||
}
|
||||
|
||||
def doDeviceSync(){
|
||||
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()
|
||||
}
|
||||
|
||||
def pollChildren() {
|
||||
def devices = getAllChildDevices()
|
||||
devices.each { d ->
|
||||
//only poll switches?
|
||||
d.poll()
|
||||
}
|
||||
private String convertHexToIP(hex) {
|
||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||
}
|
||||
|
||||
def delayPoll() {
|
||||
log.debug "Executing 'delayPoll'"
|
||||
|
||||
runIn(5, "pollChildren")
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
/*def poll() {
|
||||
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()
|
||||
{
|
||||
private Boolean canInstallLabs() {
|
||||
return hasAllHubsOver("000.011.00603")
|
||||
}
|
||||
|
||||
private Boolean hasAllHubsOver(String desiredFirmware)
|
||||
{
|
||||
private Boolean hasAllHubsOver(String desiredFirmware) {
|
||||
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
|
||||
}
|
||||
|
||||
private List getRealHubFirmwareVersions()
|
||||
{
|
||||
private List getRealHubFirmwareVersions() {
|
||||
return location.hubs*.firmwareVersionString.findAll { it }
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ definition(
|
||||
category: "Connections",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/withings.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/withings%402x.png",
|
||||
oauth: true
|
||||
oauth: true,
|
||||
singleInstance: true
|
||||
) {
|
||||
appSetting "clientId"
|
||||
appSetting "clientSecret"
|
||||
|
||||
@@ -24,7 +24,8 @@ definition(
|
||||
category: "SmartThings Internal",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png",
|
||||
oauth: true
|
||||
oauth: true,
|
||||
singleInstance: true
|
||||
) {
|
||||
appSetting "serverUrl"
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
definition(
|
||||
name: "Sprayer Controller 2",
|
||||
namespace: "",
|
||||
namespace: "sprayercontroller",
|
||||
author: "Cooper Lee",
|
||||
description: "Control Sprayers for a period of time a number of times per hour",
|
||||
category: "My Apps",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* Magic Home
|
||||
*
|
||||
* Copyright 2014 Tim Slagle
|
||||
@@ -198,7 +198,7 @@
|
||||
|
||||
//set home mode when house is occupied
|
||||
def setHome() {
|
||||
|
||||
sendOutOfDateNotification()
|
||||
log.info("Setting Home Mode!!")
|
||||
if(anyoneIsHome()) {
|
||||
if(state.sunMode == "sunset"){
|
||||
@@ -319,3 +319,14 @@
|
||||
private hideOptionsSection() {
|
||||
(starting || ending || days || modes) ? false : true
|
||||
}
|
||||
|
||||
def sendOutOfDateNotification(){
|
||||
if(!state.lastTime){
|
||||
state.lastTime = (new Date() + 31).getTime()
|
||||
sendNotification("Your version of Hello, Home Phrase Director is currently out of date. Please look for the new version of Hello, Home Phrase Director now called 'Routine Director' in the marketplace.")
|
||||
}
|
||||
else if (((new Date()).getTime()) >= state.lastTime){
|
||||
sendNotification("Your version of Hello, Home Phrase Director is currently out of date. Please look for the new version of Hello, Home Phrase Director now called 'Routine Director' in the marketplace.")
|
||||
state.lastTime = (new Date() + 31).getTime()
|
||||
}
|
||||
}
|
||||
346
smartapps/tslagle13/routine-director.src/routine-director.groovy
Normal file
346
smartapps/tslagle13/routine-director.src/routine-director.groovy
Normal file
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* Rotuine Director
|
||||
*
|
||||
*
|
||||
* Changelog
|
||||
*
|
||||
* 2015-09-01
|
||||
* --Added Contact Book
|
||||
* --Removed references to phrases and replaced with routines
|
||||
* --Added bool logic to inputs instead of enum for "yes" "no" options
|
||||
* --Fixed halting error with code installation
|
||||
*
|
||||
* Copyright 2015 Tim Slagle
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Routine Director",
|
||||
namespace: "tslagle13",
|
||||
author: "Tim Slagle",
|
||||
description: "Monitor a set of presence sensors and activate routines based on whether your home is empty or occupied. Each presence status change will check against the current 'sun state' to run routines based on occupancy and whether the sun is up or down.",
|
||||
category: "Convenience",
|
||||
iconUrl: "http://icons.iconarchive.com/icons/icons8/ios7/512/Very-Basic-Home-Filled-icon.png",
|
||||
iconX2Url: "http://icons.iconarchive.com/icons/icons8/ios7/512/Very-Basic-Home-Filled-icon.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name: "selectRoutines")
|
||||
|
||||
page(name: "Settings", title: "Settings", uninstall: true, install: true) {
|
||||
section("False alarm threshold (defaults to 10 min)") {
|
||||
input "falseAlarmThreshold", "decimal", title: "Number of minutes", required: false
|
||||
}
|
||||
|
||||
section("Zip code (for sunrise/sunset)") {
|
||||
input "zip", "text", required: true
|
||||
}
|
||||
|
||||
section("Notifications") {
|
||||
input "sendPushMessage", "bool", title: "Send notifications when house is empty?"
|
||||
input "sendPushMessageHome", "bool", title: "Send notifications when home is occupied?"
|
||||
}
|
||||
section("Send Notifications?") {
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "phone", "phone", title: "Send an SMS to this number?", required:false
|
||||
}
|
||||
}
|
||||
|
||||
section(title: "More options", hidden: hideOptionsSection(), hideable: true) {
|
||||
label title: "Assign a name", required: false
|
||||
input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
|
||||
options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||||
input "modes", "mode", title: "Only when mode is", multiple: true, required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def selectRoutines() {
|
||||
def configured = (settings.awayDay && settings.awayNight && settings.homeDay && settings.homeNight)
|
||||
dynamicPage(name: "selectRoutines", title: "Configure", nextPage: "Settings", uninstall: true) {
|
||||
section("Who?") {
|
||||
input "people", "capability.presenceSensor", title: "Monitor These Presences", required: true, multiple: true, submitOnChange: true
|
||||
}
|
||||
|
||||
def routines = location.helloHome?.getPhrases()*.label
|
||||
if (routines) {
|
||||
routines.sort()
|
||||
section("Run This Routine When...") {
|
||||
log.trace routines
|
||||
input "awayDay", "enum", title: "Everyone Is Away And It's Day", required: true, options: routines, submitOnChange: true
|
||||
input "awayNight", "enum", title: "Everyone Is Away And It's Night", required: true, options: routines, submitOnChange: true
|
||||
input "homeDay", "enum", title: "At Least One Person Is Home And It's Day", required: true, options: routines, submitOnChange: true
|
||||
input "homeNight", "enum", title: "At Least One Person Is Home And It's Night", required: true, options: routines, submitOnChange: true
|
||||
}
|
||||
/* section("Select modes used for each condition.") { This allows the director to know which rotuine has already been ran so it does not run again if someone else comes home.
|
||||
input "homeModeDay", "mode", title: "Select Mode Used for 'Home Day'", required: true
|
||||
input "homeModeNight", "mode", title: "Select Mode Used for 'Home Night'", required: true
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
subscribe(people, "presence", presence)
|
||||
checkSun()
|
||||
subscribe(location, "sunrise", setSunrise)
|
||||
subscribe(location, "sunset", setSunset)
|
||||
state.homestate = null
|
||||
}
|
||||
|
||||
//check current sun state when installed.
|
||||
def checkSun() {
|
||||
def zip = settings.zip as String
|
||||
def sunInfo = getSunriseAndSunset(zipCode: zip)
|
||||
def current = now()
|
||||
|
||||
if (sunInfo.sunrise.time < current && sunInfo.sunset.time > current) {
|
||||
state.sunMode = "sunrise"
|
||||
runIn(60,"setSunrise")
|
||||
}
|
||||
else {
|
||||
state.sunMode = "sunset"
|
||||
runIn(60,"setSunset")
|
||||
}
|
||||
}
|
||||
|
||||
//change to sunrise mode on sunrise event
|
||||
def setSunrise(evt) {
|
||||
state.sunMode = "sunrise";
|
||||
changeSunMode(newMode);
|
||||
log.debug "Current sun mode is ${state.sunMode}"
|
||||
}
|
||||
|
||||
//change to sunset mode on sunset event
|
||||
def setSunset(evt) {
|
||||
state.sunMode = "sunset";
|
||||
changeSunMode(newMode)
|
||||
log.debug "Current sun mode is ${state.sunMode}"
|
||||
}
|
||||
|
||||
//change mode on sun event
|
||||
def changeSunMode(newMode) {
|
||||
if (allOk) {
|
||||
|
||||
if (everyoneIsAway()) /*&& (state.sunMode == "sunrise")*/ {
|
||||
log.info("Home is Empty Setting New Away Mode")
|
||||
def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60
|
||||
setAway()
|
||||
}
|
||||
/*
|
||||
else if (everyoneIsAway() && (state.sunMode == "sunset")) {
|
||||
log.info("Home is Empty Setting New Away Mode")
|
||||
def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60
|
||||
setAway()
|
||||
}*/
|
||||
else if (anyoneIsHome()) {
|
||||
log.info("Home is Occupied Setting New Home Mode")
|
||||
setHome()
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//presence change run logic based on presence state of home
|
||||
def presence(evt) {
|
||||
if (allOk) {
|
||||
if (evt.value == "not present") {
|
||||
log.debug("Checking if everyone is away")
|
||||
|
||||
if (everyoneIsAway()) {
|
||||
log.info("Nobody is home, running away sequence")
|
||||
def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60
|
||||
runIn(delay, "setAway")
|
||||
}
|
||||
}
|
||||
else {
|
||||
def lastTime = state[evt.deviceId]
|
||||
if (lastTime == null || now() - lastTime >= 1 * 60000) {
|
||||
log.info("Someone is home, running home sequence")
|
||||
setHome()
|
||||
}
|
||||
state[evt.deviceId] = now()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//if empty set home to one of the away modes
|
||||
def setAway() {
|
||||
if (everyoneIsAway()) {
|
||||
if (state.sunMode == "sunset") {
|
||||
def message = "Performing \"${awayNight}\" for you as requested."
|
||||
log.info(message)
|
||||
sendAway(message)
|
||||
location.helloHome.execute(settings.awayNight)
|
||||
state.homestate = "away"
|
||||
|
||||
}
|
||||
else if (state.sunMode == "sunrise") {
|
||||
def message = "Performing \"${awayDay}\" for you as requested."
|
||||
log.info(message)
|
||||
sendAway(message)
|
||||
location.helloHome.execute(settings.awayDay)
|
||||
state.homestate = "away"
|
||||
}
|
||||
else {
|
||||
log.debug("Mode is the same, not evaluating")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//set home mode when house is occupied
|
||||
def setHome() {
|
||||
log.info("Setting Home Mode!!")
|
||||
if (anyoneIsHome()) {
|
||||
if (state.sunMode == "sunset") {
|
||||
if (state.homestate != "homeNight") {
|
||||
def message = "Performing \"${homeNight}\" for you as requested."
|
||||
log.info(message)
|
||||
sendHome(message)
|
||||
location.helloHome.execute(settings.homeNight)
|
||||
state.homestate = "homeNight"
|
||||
}
|
||||
}
|
||||
|
||||
if (state.sunMode == "sunrise") {
|
||||
if (state.homestate != "homeDay") {
|
||||
def message = "Performing \"${homeDay}\" for you as requested."
|
||||
log.info(message)
|
||||
sendHome(message)
|
||||
location.helloHome.execute(settings.homeDay)
|
||||
state.homestate = "homeDay"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private everyoneIsAway() {
|
||||
def result = true
|
||||
|
||||
if(people.findAll { it?.currentPresence == "present" }) {
|
||||
result = false
|
||||
}
|
||||
|
||||
log.debug("everyoneIsAway: ${result}")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private anyoneIsHome() {
|
||||
def result = false
|
||||
|
||||
if(people.findAll { it?.currentPresence == "present" }) {
|
||||
result = true
|
||||
}
|
||||
|
||||
log.debug("anyoneIsHome: ${result}")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
def sendAway(msg) {
|
||||
if (sendPushMessage) {
|
||||
if (recipients) {
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
sendPush(msg)
|
||||
if(phone){
|
||||
sendSms(phone, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.debug(msg)
|
||||
}
|
||||
|
||||
def sendHome(msg) {
|
||||
if (sendPushMessageHome) {
|
||||
if (recipients) {
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
}
|
||||
else {
|
||||
sendPush(msg)
|
||||
if(phone){
|
||||
sendSms(phone, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.debug(msg)
|
||||
}
|
||||
|
||||
private getAllOk() {
|
||||
modeOk && daysOk && timeOk
|
||||
}
|
||||
|
||||
private getModeOk() {
|
||||
def result = !modes || modes.contains(location.mode)
|
||||
log.trace "modeOk = $result"
|
||||
result
|
||||
}
|
||||
|
||||
private getDaysOk() {
|
||||
def result = true
|
||||
if (days) {
|
||||
def df = new java.text.SimpleDateFormat("EEEE")
|
||||
if (location.timeZone) {
|
||||
df.setTimeZone(location.timeZone)
|
||||
}
|
||||
else {
|
||||
df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
|
||||
}
|
||||
def day = df.format(new Date())
|
||||
result = days.contains(day)
|
||||
}
|
||||
log.trace "daysOk = $result"
|
||||
result
|
||||
}
|
||||
|
||||
private getTimeOk() {
|
||||
def result = true
|
||||
if (starting && ending) {
|
||||
def currTime = now()
|
||||
def start = timeToday(starting, location?.timeZone).time
|
||||
def stop = timeToday(ending, location?.timeZone).time
|
||||
result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
|
||||
}
|
||||
log.trace "timeOk = $result"
|
||||
result
|
||||
}
|
||||
|
||||
private hhmm(time, fmt = "h:mm a") {
|
||||
def t = timeToday(time, location.timeZone)
|
||||
def f = new java.text.SimpleDateFormat(fmt)
|
||||
f.setTimeZone(location.timeZone?:timeZone(time))
|
||||
f.format(t)
|
||||
}
|
||||
|
||||
private getTimeIntervalLabel() {
|
||||
(starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z"): ""
|
||||
}
|
||||
|
||||
private hideOptionsSection() {
|
||||
(starting || ending || days || modes) ? false: true
|
||||
}
|
||||
@@ -58,7 +58,8 @@ definition(
|
||||
description: "Connect your Quirky to SmartThings.",
|
||||
category: "SmartThings Labs",
|
||||
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 "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.",
|
||||
category: "SmartThings Labs",
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
|
||||
307
smartapps/weatherbug/weatherbug-home.src/weatherbug-home.groovy
Normal file
307
smartapps/weatherbug/weatherbug-home.src/weatherbug-home.groovy
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* WeatherBug Home
|
||||
*
|
||||
* Copyright 2015 WeatherBug
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "WeatherBug Home",
|
||||
namespace: "WeatherBug",
|
||||
author: "WeatherBug Home",
|
||||
description: "WeatherBug Home",
|
||||
category: "My Apps",
|
||||
iconUrl: "http://stg.static.myenergy.enqa.co/apps/wbhc/v2/images/weatherbughomemedium.png",
|
||||
iconX2Url: "http://stg.static.myenergy.enqa.co/apps/wbhc/v2/images/weatherbughomemedium.png",
|
||||
iconX3Url: "http://stg.static.myenergy.enqa.co/apps/wbhc/v2/images/weatherbughome.png",
|
||||
oauth: [displayName: "WeatherBug Home", displayLink: "http://weatherbughome.com/"])
|
||||
|
||||
|
||||
preferences {
|
||||
section("Select thermostats") {
|
||||
input "thermostatDevice", "capability.thermostat", multiple: true
|
||||
}
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/appInfo") { action: [ GET: "getAppInfo" ] }
|
||||
path("/getLocation") { action: [ GET: "getLoc" ] }
|
||||
path("/currentReport/:id") { action: [ GET: "getCurrentReport" ] }
|
||||
path("/setTemp/:temp/:id") { action: [ POST: "setTemperature", GET: "setTemperature" ] }
|
||||
}
|
||||
|
||||
/**
|
||||
* This API call will be leveraged by a WeatherBug Home Service to retrieve
|
||||
* data from the installed SmartApp, including the location data, and
|
||||
* a list of the devices that were authorized to be accessed. The WeatherBug
|
||||
* Home Service will leverage this data to represent the connected devices as well as their
|
||||
* location and associated the data with a WeatherBug user account.
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
* @return Location, including id, latitude, longitude, zip code, and name, and the list of devices
|
||||
*/
|
||||
def getAppInfo() {
|
||||
def devices = thermostatDevice
|
||||
def lat = location.latitude
|
||||
def lon = location.longitude
|
||||
if(!(devices instanceof Collection))
|
||||
{
|
||||
devices = [devices]
|
||||
}
|
||||
return [
|
||||
Id: UUID.randomUUID().toString(),
|
||||
Code: 200,
|
||||
ErrorMessage: null,
|
||||
Result: [ "Devices": devices,
|
||||
"Location":[
|
||||
"Id": location.id,
|
||||
"Latitude":lat,
|
||||
"Longitude":lon,
|
||||
"ZipCode":location.zipCode,
|
||||
"Name":location.name
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* This API call will be leveraged by a WeatherBug Home Service to retrieve
|
||||
* location data from the installed SmartApp. The WeatherBug
|
||||
* Home Service will leverage this data to associate the location to a WeatherBug Home account
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
*
|
||||
* @return Location, including id, latitude, longitude, zip code, and name
|
||||
*/
|
||||
def getLoc() {
|
||||
return [
|
||||
Id: UUID.randomUUID().toString(),
|
||||
Code: 200,
|
||||
ErrorMessage: null,
|
||||
Result: [
|
||||
"Id": location.id,
|
||||
"Latitude":location.latitude,
|
||||
"Longitude":location.longitude,
|
||||
"ZipCode":location.zipCode,
|
||||
"Name":location.name]
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* This API call will be leveraged by a WeatherBug Home Service to retrieve
|
||||
* thermostat data and store it for display to a WeatherBug user.
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
*
|
||||
* @param id The id of the device to get data for
|
||||
* @return Thermostat data including temperature, set points, running modes, and operating states
|
||||
*/
|
||||
def getCurrentReport() {
|
||||
log.debug "device id parameter=" + params.id
|
||||
def unixTime = (int)((new Date().getTime() / 1000))
|
||||
def device = thermostatDevice.find{ it.id == params.id}
|
||||
|
||||
if(device == null)
|
||||
{
|
||||
return [
|
||||
Id: UUID.randomUUID().toString(),
|
||||
Code: 404,
|
||||
ErrorMessage: "Device not found. id=" + params.id,
|
||||
Result: null
|
||||
]
|
||||
}
|
||||
return [
|
||||
Id: UUID.randomUUID().toString(),
|
||||
Code: 200,
|
||||
ErrorMessage: null,
|
||||
Result: [
|
||||
DeviceId: device.id,
|
||||
LocationId: location.id,
|
||||
ReportType: 2,
|
||||
ReportList: [
|
||||
[Key: "Temperature", Value: GetOrDefault(device, "temperature")],
|
||||
[Key: "ThermostatSetpoint", Value: GetOrDefault(device, "thermostatSetpoint")],
|
||||
[Key: "CoolingSetpoint", Value: GetOrDefault(device, "coolingSetpoint")],
|
||||
[Key: "HeatingSetpoint", Value: GetOrDefault(device, "heatingSetpoint")],
|
||||
[Key: "ThermostatMode", Value: GetOrDefault(device, "thermostatMode")],
|
||||
[Key: "ThermostatFanMode", Value: GetOrDefault(device, "thermostatFanMode")],
|
||||
[Key: "ThermostatOperatingState", Value: GetOrDefault(device, "thermostatOperatingState")]
|
||||
],
|
||||
UnixTime: unixTime
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* This API call will be leveraged by a WeatherBug Home Service to set
|
||||
* the thermostat setpoint.
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
*
|
||||
* @param id The id of the device to set
|
||||
* @return Indication of whether the operation succeeded or failed
|
||||
|
||||
def setTemperature() {
|
||||
log.debug "device id parameter=" + params.id
|
||||
def device = thermostatDevice.find{ it.id == params.id}
|
||||
if(device != null)
|
||||
{
|
||||
def mode = device.latestState('thermostatMode').stringValue
|
||||
def value = params.temp as Integer
|
||||
log.trace "Suggested temperature: $value, $mode"
|
||||
if ( mode == "cool")
|
||||
device.setCoolingSetpoint(value)
|
||||
else if ( mode == "heat")
|
||||
device.setHeatingSetpoint(value)
|
||||
return [
|
||||
Id: UUID.randomUUID().toString(),
|
||||
Code: 200,
|
||||
ErrorMessage: null,
|
||||
Result: null
|
||||
]
|
||||
}
|
||||
return [
|
||||
Id: UUID.randomUUID().toString(),
|
||||
Code : 404,
|
||||
ErrorMessage: "Device not found. id=" + params.id,
|
||||
Result: null
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
initialize()
|
||||
}
|
||||
|
||||
/**
|
||||
* The updated event will be pushed to a WeatherBug Home Service to notify the system to take appropriate action.
|
||||
* Data that will be sent includes the list of devices, and location data
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
*/
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
log.debug "Updated with state: ${state}"
|
||||
log.debug "Updated with location ${location} ${location.id} ${location.name}"
|
||||
unsubscribe()
|
||||
initialize()
|
||||
def postParams = [
|
||||
uri: 'https://smartthingsrec.api.earthnetworks.com/api/v1/receive/smartapp/update',
|
||||
body: [
|
||||
"Devices": devices,
|
||||
"Location":[
|
||||
"Id": location.id,
|
||||
"Latitude":location.latitude,
|
||||
"Longitude":location.longitude,
|
||||
"ZipCode":location.zipCode,
|
||||
"Name":location.name
|
||||
]
|
||||
]
|
||||
]
|
||||
sendToWeatherBug(postParams)
|
||||
}
|
||||
|
||||
/*
|
||||
* Subscribe to changes on the thermostat attributes
|
||||
*/
|
||||
def initialize() {
|
||||
log.trace "initialize enter"
|
||||
subscribe(thermostatDevice, "heatingSetpoint", pushLatest)
|
||||
subscribe(thermostatDevice, "coolingSetpoint", pushLatest)
|
||||
subscribe(thermostatDevice, "thermostatSetpoint", pushLatest)
|
||||
subscribe(thermostatDevice, "thermostatMode", pushLatest)
|
||||
subscribe(thermostatDevice, "thermostatFanMode", pushLatest)
|
||||
subscribe(thermostatDevice, "thermostatOperatingState", pushLatest)
|
||||
subscribe(thermostatDevice, "temperature", pushLatest)
|
||||
}
|
||||
|
||||
/**
|
||||
* The uninstall event will be pushed to a WeatherBug Home Service to notify the system to take appropriate action.
|
||||
* Data that will be sent includes the list of devices, and location data
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
*/
|
||||
def uninstalled() {
|
||||
log.trace "uninstall entered"
|
||||
def postParams = [
|
||||
uri: 'https://smartthingsrec.api.earthnetworks.com/api/v1/receive/smartapp/delete',
|
||||
body: [
|
||||
"Devices": devices,
|
||||
"Location":[
|
||||
"Id": location.id,
|
||||
"Latitude":location.latitude,
|
||||
"Longitude":location.longitude,
|
||||
"ZipCode":location.zipCode,
|
||||
"Name":location.name
|
||||
]
|
||||
]
|
||||
]
|
||||
sendToWeatherBug(postParams)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will push the latest thermostat data to the WeatherBug Home Service so it can store
|
||||
* and display the data to the WeatherBug user. Data pushed includes the thermostat data as well
|
||||
* as location id.
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
*/
|
||||
def pushLatest(evt) {
|
||||
def unixTime = (int)((new Date().getTime() / 1000))
|
||||
def device = thermostatDevice.find{ it.id == evt.deviceId}
|
||||
def postParams = [
|
||||
uri: 'https://smartthingsrec.api.earthnetworks.com/api/v1/receive',
|
||||
body: [
|
||||
DeviceId: evt.deviceId,
|
||||
LocationId: location.id,
|
||||
ReportType: 2,
|
||||
ReportList: [
|
||||
[Key: "Temperature", Value: GetOrDefault(device, "temperature")],
|
||||
[Key: "ThermostatSetpoint", Value: GetOrDefault(device, "thermostatSetpoint")],
|
||||
[Key: "CoolingSetpoint", Value: GetOrDefault(device, "coolingSetpoint")],
|
||||
[Key: "HeatingSetpoint", Value: GetOrDefault(device, "heatingSetpoint")],
|
||||
[Key: "ThermostatMode", Value: GetOrDefault(device, "thermostatMode")],
|
||||
[Key: "ThermostatFanMode", Value: GetOrDefault(device, "thermostatFanMode")],
|
||||
[Key: "ThermostatOperatingState", Value: GetOrDefault(device, "thermostatOperatingState")]
|
||||
],
|
||||
UnixTime: unixTime
|
||||
]
|
||||
]
|
||||
log.debug postParams
|
||||
sendToWeatherBug(postParams)
|
||||
}
|
||||
|
||||
/*
|
||||
* This method attempts to get the value of a device attribute, but if an error occurs null is returned
|
||||
* @return The device attribute value, or null
|
||||
*/
|
||||
def GetOrDefault(device, attrib)
|
||||
{
|
||||
def val
|
||||
try{
|
||||
val = device.latestValue(attrib)
|
||||
|
||||
}catch(ex)
|
||||
{
|
||||
log.debug "Failed to get attribute " + attrib + " from device " + device
|
||||
val = null
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
/*
|
||||
* Convenience method that sends data to WeatherBug, logging any exceptions that may occur
|
||||
* Privacy Policy: http://weatherbughome.com/privacy/
|
||||
*/
|
||||
def sendToWeatherBug(postParams)
|
||||
{
|
||||
try{
|
||||
log.debug postParams
|
||||
httpPostJson(postParams) { resp ->
|
||||
resp.headers.each {
|
||||
log.debug "${it.name} : ${it.value}"
|
||||
}
|
||||
log.debug "response contentType: ${resp.contentType}"
|
||||
log.debug "response data: ${resp.data}"
|
||||
}
|
||||
log.debug "Communication with WeatherBug succeeded";
|
||||
|
||||
}catch(ex)
|
||||
{
|
||||
log.debug "Communication with WeatherBug failed.\n${ex}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user