mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-09 05:11:52 +00:00
Compare commits
118 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
29f315ac33 | ||
|
|
2baac34ba3 | ||
|
|
811a1af4bf | ||
|
|
b765b46c50 | ||
|
|
6eac6affcf | ||
|
|
d2cecf6908 | ||
|
|
61d2aac45a | ||
|
|
b5d0a5e74b | ||
|
|
1c2189b63c | ||
|
|
1adb4000a6 | ||
|
|
bad978afbd | ||
|
|
80b4d6a665 | ||
|
|
f51d6542b8 | ||
|
|
7baad1c35e | ||
|
|
621bcfadd2 | ||
|
|
83bbaebef2 | ||
|
|
bde5abcdb5 | ||
|
|
99e48dbeed | ||
|
|
2ee1b26a7f | ||
|
|
a7ed8f4afe | ||
|
|
148ee3521d | ||
|
|
d7490a086a | ||
|
|
189bec58db | ||
|
|
7e26fd1040 | ||
|
|
a244808073 | ||
|
|
33f1209c80 | ||
|
|
d977c4d46b | ||
|
|
01b2f57d7a | ||
|
|
ab00d703bf | ||
|
|
6c3a7886ed | ||
|
|
5719bbcaac | ||
|
|
324d9bf780 | ||
|
|
eb8d861c6c | ||
|
|
99a4d75e4b | ||
|
|
d601484398 | ||
|
|
a3fbc8ebd8 | ||
|
|
bfc14ffd9e | ||
|
|
1c327d1433 | ||
|
|
deee914573 | ||
|
|
f6b541c30f | ||
|
|
c00fbd3652 | ||
|
|
76f056180d | ||
|
|
c39b63b944 | ||
|
|
40bf47ec0b | ||
|
|
dc2ac4bedc | ||
|
|
0cf90064ec | ||
|
|
0497660ab5 | ||
|
|
6005f7266e | ||
|
|
5d38cabe75 | ||
|
|
29f94ee6ac | ||
|
|
8929673ff0 | ||
|
|
94228c258a | ||
|
|
1b424a8ea8 | ||
|
|
35a7a79073 | ||
|
|
8dd3b2396f | ||
|
|
39d2def035 | ||
|
|
e1a9f2f761 | ||
|
|
37eb8cc0a1 | ||
|
|
402b0be80b | ||
|
|
a761590322 | ||
|
|
e56086aac3 | ||
|
|
6b142622db | ||
|
|
8d07e81b80 | ||
|
|
0fcff53eba |
@@ -0,0 +1,506 @@
|
||||
/**
|
||||
* Keen Home Smart Vent
|
||||
*
|
||||
* Author: Keen Home
|
||||
* Date: 2015-06-23
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "Keen Home Smart Vent", namespace: "Keen Home", author: "Gregg Altschul") {
|
||||
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"
|
||||
|
||||
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: "switch.off", icon:"st.vents.vent-closed", backgroundColor:"#ff0000"
|
||||
}
|
||||
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)
|
||||
// log.debug "rawValue: ${rawValue}"
|
||||
def value = Integer.parseInt(rawValue, 16)
|
||||
def rangeMax = 254
|
||||
|
||||
if (value == 255) {
|
||||
log.debug "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."
|
||||
]
|
||||
} else if ( device.currentValue("switch") == "obstructed" &&
|
||||
value == 254) {
|
||||
// When the device is reset after an obstruction, the switch
|
||||
// state will be obstructed and the value coming from the device
|
||||
// will be 254. Since we're not using heating/cooling mode from
|
||||
// the device type handler, we need to bump it down to the lower
|
||||
// (cooling) range
|
||||
sendEvent(makeOnOffResult(1)) // clear the obstructed switch state
|
||||
value = rangeMax
|
||||
}
|
||||
// else if (device.currentValue("switch") == "off") {
|
||||
// sendEvent(makeOnOffResult(1)) // turn back on if in off state
|
||||
// }
|
||||
|
||||
|
||||
// log.debug "pre-value: ${value}"
|
||||
value = Math.floor(value / rangeMax * 100)
|
||||
// log.debug "post-value: ${value}"
|
||||
|
||||
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}" ]
|
||||
}
|
||||
/**** COMMAND METHODS ****/
|
||||
// def mfgCode() {
|
||||
// ["zcl mfg-code 0x115B", "delay 200"]
|
||||
// }
|
||||
|
||||
def on() {
|
||||
log.debug "on()"
|
||||
sendEvent(makeOnOffResult(1))
|
||||
"st cmd 0x${device.deviceNetworkId} 1 6 1 {}"
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "off()"
|
||||
sendEvent(makeOnOffResult(0))
|
||||
"st cmd 0x${device.deviceNetworkId} 1 6 0 {}"
|
||||
}
|
||||
|
||||
// does this work?
|
||||
def toggle() {
|
||||
log.debug "toggle()"
|
||||
|
||||
"st cmd 0x${device.deviceNetworkId} 1 6 2 {}"
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
log.debug "setting level: ${value}"
|
||||
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
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")
|
||||
}
|
||||
def rangeMax = 254
|
||||
def computedLevel = Math.round(value * rangeMax / 100)
|
||||
log.debug "computedLevel: ${computedLevel}"
|
||||
|
||||
def level = new BigInteger(computedLevel.toString()).toString(16)
|
||||
log.debug "level: ${level}"
|
||||
|
||||
if (level.size() < 2){
|
||||
level = '0' + level
|
||||
}
|
||||
|
||||
"st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} 0000}"
|
||||
}
|
||||
|
||||
|
||||
def getOnOff() {
|
||||
log.debug "getOnOff()"
|
||||
|
||||
["st rattr 0x${device.deviceNetworkId} 1 0x0006 0"]
|
||||
}
|
||||
|
||||
def getPressure() {
|
||||
log.debug "getPressure()"
|
||||
[
|
||||
"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()"
|
||||
// rattr = read attribute
|
||||
// 0x${} = device net id
|
||||
// 1 = endpoint
|
||||
// 8 = cluster id (level control, in this case)
|
||||
// 0 = attribute within cluster
|
||||
// sendEvent(name: "level", value: value)
|
||||
["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()
|
||||
}
|
||||
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
int i = 0;
|
||||
int j = array.length - 1;
|
||||
byte tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "CONFIGURE"
|
||||
log.debug "zigbeeId: ${device.hub.zigbeeId}"
|
||||
|
||||
setZigBeeIdTile()
|
||||
|
||||
def configCmds = [
|
||||
// binding commands
|
||||
"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
|
||||
// [cluster] [attr] [type] [min-interval] [max-interval] [min-change]
|
||||
|
||||
// mike 2015/06/22: preconfigured; see tech spec
|
||||
// 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",
|
||||
|
||||
// mike 2015/06/22: preconfigured; see tech spec
|
||||
// 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",
|
||||
|
||||
// mike 2015/06/22: temp and pressure reports are preconfigured, but
|
||||
// we'd like to override their settings for our own purposes
|
||||
// temperature - type: int16s, change: 0xA = 10 = 0.1C
|
||||
"zcl global send-me-a-report 0x0402 0 0x29 10 60 {0A00}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
// mike 2015/06/22: use new custom pressure attribute
|
||||
// pressure - type: int32u, change: 1 = 0.1Pa
|
||||
"zcl mfg-code 0x115B", "delay 200",
|
||||
"zcl global send-me-a-report 0x0403 0x20 0x22 10 60 {010000}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500"
|
||||
|
||||
// mike 2015/06/22: preconfigured; see tech spec
|
||||
// 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()
|
||||
}
|
||||
@@ -56,7 +56,7 @@ metadata {
|
||||
state "configure", label: '', action: "configuration.configure", icon: "st.secondary.configure"
|
||||
}
|
||||
|
||||
PLATFORM_graphTile(name: "powerGraph", attribute: "device.power")
|
||||
graphTile(name: "powerGraph", attribute: "device.power")
|
||||
|
||||
main(["power", "energy"])
|
||||
details(["powerGraph", "power", "energy", "reset", "refresh", "configure"])
|
||||
@@ -68,16 +68,8 @@ metadata {
|
||||
// ========================================================
|
||||
|
||||
preferences {
|
||||
input name: "graphPrecision", type: "enum", title: "Graph Precision", description: "Daily", required: true, options: PLATFORM_graphPrecisionOptions(), defaultValue: "Daily"
|
||||
input name: "graphType", type: "enum", title: "Graph Type", description: selectedGraphType(), required: false, options: PLATFORM_graphTypeOptions()
|
||||
}
|
||||
|
||||
def selectedGraphPrecision() {
|
||||
graphPrecision ?: "Daily"
|
||||
}
|
||||
|
||||
def selectedGraphType() {
|
||||
graphType ?: "line"
|
||||
input name: "graphPrecision", type: "enum", title: "Graph Precision", description: "Daily", required: true, options: graphPrecisionOptions(), defaultValue: "Daily"
|
||||
input name: "graphType", type: "enum", title: "Graph Type", description: "line", required: false, options: graphTypeOptions()
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
@@ -91,22 +83,6 @@ mappings {
|
||||
GET: "renderGraph"
|
||||
]
|
||||
}
|
||||
path("/graphDataSizes") { // for testing. remove before publishing
|
||||
action:
|
||||
[
|
||||
GET: "graphDataSizes"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def graphDataSizes() { // for testing. remove before publishing
|
||||
state.findAll { k, v -> k.startsWith("measure.") }.inject([:]) { attributes, attributeData ->
|
||||
attributes[attributeData.key] = attributeData.value.inject([:]) { dateTypes, dateTypeData ->
|
||||
dateTypes[dateTypeData.key] = dateTypeData.value.size()
|
||||
dateTypes
|
||||
}
|
||||
attributes
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
@@ -121,8 +97,7 @@ def parse(String description) {
|
||||
}
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
|
||||
PLATFORM_migrateGraphDataIfNeeded()
|
||||
PLATFORM_storeData(result.name, result.value)
|
||||
storeGraphData(result.name, result.value)
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -176,535 +151,15 @@ def configure() {
|
||||
|
||||
def renderGraph() {
|
||||
|
||||
def data = PLATFORM_fetchGraphData(params.attribute)
|
||||
def data = fetchGraphData(params.attribute)
|
||||
|
||||
def totalData = data*.runningSum
|
||||
|
||||
def xValues = data*.unixTime
|
||||
|
||||
def yValues = [
|
||||
Total: [color: "#49a201", data: totalData, type: selectedGraphType()]
|
||||
Total: [color: "#49a201", data: totalData]
|
||||
]
|
||||
|
||||
PLATFORM_renderGraph(attribute: params.attribute, xValues: xValues, yValues: yValues, focus: "Total", label: "Watts")
|
||||
}
|
||||
|
||||
// TODO: // ========================================================
|
||||
// TODO: // PLATFORM CODE !!! DO NOT ALTER !!!
|
||||
// TODO: // ========================================================
|
||||
|
||||
// ========================================================
|
||||
// PLATFORM TILES
|
||||
// ========================================================
|
||||
|
||||
def PLATFORM_graphTile(Map tileParams) {
|
||||
def cleanAttribute = tileParams.attribute - "device." - "capability."
|
||||
htmlTile([name: tileParams.name, attribute: tileParams.attribute, action: "graph/${cleanAttribute}", width: 3, height: 2] + tileParams)
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// PLATFORM GRAPH RENDERING
|
||||
// ========================================================
|
||||
|
||||
private PLATFORM_graphTypeOptions() {
|
||||
[
|
||||
"line", // DEFAULT
|
||||
"spline",
|
||||
"step",
|
||||
"area",
|
||||
"area-spline",
|
||||
"area-step",
|
||||
"bar",
|
||||
"scatter",
|
||||
"pie",
|
||||
"donut",
|
||||
"gauge",
|
||||
]
|
||||
}
|
||||
|
||||
private PLATFORM_renderGraph(graphParams) {
|
||||
|
||||
String attribute = graphParams.attribute
|
||||
List xValues = graphParams.xValues
|
||||
Map yValues = graphParams.yValues
|
||||
String focus = graphParams.focus ?: ""
|
||||
String label = graphParams.label ?: ""
|
||||
|
||||
/*
|
||||
def xValues = [1, 2]
|
||||
|
||||
def yValues = [
|
||||
High: [type: "spline", data: [5, 6], color: "#bc2323"],
|
||||
Low: [type: "spline", data: [0, 1], color: "#153591"]
|
||||
]
|
||||
|
||||
Available type values:
|
||||
line // DEFAULT
|
||||
spline
|
||||
step
|
||||
area
|
||||
area-spline
|
||||
area-step
|
||||
bar
|
||||
scatter
|
||||
pie
|
||||
donut
|
||||
gauge
|
||||
|
||||
*/
|
||||
|
||||
def graphData = PLATFORM_buildGraphData(xValues, yValues, label)
|
||||
|
||||
def legendData = yValues*.key
|
||||
def focusJS = focus ? "chart.focus('${focus}')" : "// focus not specified"
|
||||
def flowColumn = focus ?: yValues ? yValues.keySet().first() : null
|
||||
|
||||
def htmlTitle = "${(device.label ?: device.name)} ${attribute.capitalize()} Graph"
|
||||
renderHTML(htmlTitle) { html ->
|
||||
html.head {
|
||||
"""
|
||||
<!-- Load c3.css -->
|
||||
<link href="https://www.dropbox.com/s/m6ptp72cw4nx0sp/c3.css?dl=1" rel="stylesheet" type="text/css">
|
||||
|
||||
<!-- Load d3.js and c3.js -->
|
||||
<script src="https://www.dropbox.com/s/9x22jyfu5qyacpp/d3.v3.min.js?dl=1" charset="utf-8"></script>
|
||||
<script src="https://www.dropbox.com/s/to7dtcn403l7mza/c3.js?dl=1"></script>
|
||||
|
||||
<script>
|
||||
function getDocumentHeight() {
|
||||
var body = document.body;
|
||||
var html = document.documentElement;
|
||||
|
||||
return html.clientHeight;
|
||||
}
|
||||
function getDocumentWidth() {
|
||||
var body = document.body;
|
||||
var html = document.documentElement;
|
||||
|
||||
return html.clientWidth;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.legend {
|
||||
position: absolute;
|
||||
width: 80%;
|
||||
padding-left: 15%;
|
||||
z-index: 999;
|
||||
padding-top: 5px;
|
||||
}
|
||||
.legend span {
|
||||
width: ${100 / yValues.size()}%;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
}
|
||||
html.body {
|
||||
"""
|
||||
<div class="legend"></div>
|
||||
<div id="chart" style="max-height: 120px; position: relative;"></div>
|
||||
|
||||
<script>
|
||||
|
||||
// Generate the chart
|
||||
var chart = c3.generate(${graphData as grails.converters.JSON});
|
||||
|
||||
// Resize the chart to the size of the device tile
|
||||
chart.resize({height:getDocumentHeight(), width:getDocumentWidth()});
|
||||
|
||||
// Focus data if specified
|
||||
${focusJS}
|
||||
|
||||
// Update the chart when ${attribute} events are received
|
||||
function ${attribute}(evt) {
|
||||
var newValue = ['${flowColumn}'];
|
||||
newValue.push(evt.value);
|
||||
|
||||
var newX = ['x'];
|
||||
newX.push(evt.unixTime);
|
||||
|
||||
chart.flow({
|
||||
columns: [
|
||||
newX,
|
||||
newValue
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// Build the custom legend
|
||||
d3.select('.legend').selectAll('span')
|
||||
.data(${legendData as grails.converters.JSON})
|
||||
.enter().append('span')
|
||||
.attr('data-id', function (id) { return id; })
|
||||
.html(function (id) { return id; })
|
||||
.each(function (id) {
|
||||
d3.select(this).style('background-color', chart.color(id));
|
||||
})
|
||||
.on('mouseover', function (id) {
|
||||
chart.focus(id);
|
||||
})
|
||||
.on('mouseout', function (id) {
|
||||
chart.revert();
|
||||
})
|
||||
.on('click', function (id) {
|
||||
chart.toggle(id);
|
||||
});
|
||||
|
||||
</script>
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PLATFORM_buildGraphData(List xValues, Map yValues, String label = "") {
|
||||
|
||||
/*
|
||||
def xValues = [1, 2]
|
||||
|
||||
def yValues = [
|
||||
High: [type: "spline", data: [5, 6], color: "#bc2323"],
|
||||
Low: [type: "spline", data: [0, 1], color: "#153591"]
|
||||
]
|
||||
*/
|
||||
|
||||
[
|
||||
interaction: [
|
||||
enabled: false
|
||||
],
|
||||
bindto : '#chart',
|
||||
padding : [
|
||||
left : 30,
|
||||
right : 30,
|
||||
bottom: 0,
|
||||
top : 0
|
||||
],
|
||||
legend : [
|
||||
show: false,
|
||||
// hide : false,//(yValues.keySet().size() < 2),
|
||||
// position: 'inset',
|
||||
// inset: [
|
||||
// anchor: "top-right"
|
||||
// ],
|
||||
// item: [
|
||||
// onclick: "do nothing" // (yValues.keySet().size() > 1) ? null : "do nothing"
|
||||
// ]
|
||||
],
|
||||
data : [
|
||||
x : "x",
|
||||
columns: [(["x"] + xValues)] + yValues.collect { k, v -> [k] + v.data },
|
||||
types : yValues.inject([:]) { total, current -> total[current.key] = current.value.type; return total },
|
||||
colors : yValues.inject([:]) { total, current -> total[current.key] = current.value.color; return total }
|
||||
],
|
||||
axis : [
|
||||
x: [
|
||||
type: 'timeseries',
|
||||
tick: [
|
||||
centered: true,
|
||||
culling : [max: 7],
|
||||
fit : true,
|
||||
format : PLATFORM_getGraphDateFormat()
|
||||
// format: PLATFORM_getGraphDateFormatFunction() // throws securityException when trying to escape javascript
|
||||
]
|
||||
],
|
||||
y: [
|
||||
label : label,
|
||||
padding: [
|
||||
top: 50
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
private PLATFORM_getGraphDateFormat(dateType = selectedGraphPrecision()) {
|
||||
// https://github.com/mbostock/d3/wiki/Time-Formatting
|
||||
def graphDateFormat
|
||||
switch (dateType) {
|
||||
case "Live":
|
||||
graphDateFormat = "%I:%M" // hour (12-hour clock) as a decimal number [00,12] // AM or PM
|
||||
break
|
||||
case "Hourly":
|
||||
graphDateFormat = "%I %p" // hour (12-hour clock) as a decimal number [00,12] // AM or PM
|
||||
break
|
||||
case "Daily":
|
||||
graphDateFormat = "%a" // abbreviated weekday name
|
||||
break
|
||||
case "Monthly":
|
||||
graphDateFormat = "%b" // abbreviated month name
|
||||
break
|
||||
case "Annually":
|
||||
graphDateFormat = "%y" // year without century as a decimal number [00,99]
|
||||
break
|
||||
}
|
||||
graphDateFormat
|
||||
}
|
||||
|
||||
private String PLATFORM_getGraphDateFormatFunction(dateType = selectedGraphPrecision()) {
|
||||
def graphDateFunction = "function(date) { return date; }"
|
||||
switch (dateType) {
|
||||
case "Live":
|
||||
graphDateFunction = """
|
||||
function(date) {
|
||||
return.getMinutes();
|
||||
}
|
||||
"""
|
||||
break;
|
||||
case "Hourly":
|
||||
graphDateFunction = """ function(date) {
|
||||
var hour = date.getHours();
|
||||
if (hour == 0) {
|
||||
return String(/12 am/).substring(1).slice(0,-1);
|
||||
} else if (hour > 12) {
|
||||
return hour -12 + String(/ pm/).substring(1).slice(0,-1);
|
||||
} else {
|
||||
return hour + String(/ am/).substring(1).slice(0,-1);
|
||||
}
|
||||
}"""
|
||||
break
|
||||
case "Daily":
|
||||
graphDateFunction = """ function(date) {
|
||||
var day = date.getDay();
|
||||
switch(day) {
|
||||
case 0: return String(/Sun/).substring(1).slice(0,-1);
|
||||
case 1: return String(/Mon/).substring(1).slice(0,-1);
|
||||
case 2: return String(/Tue/).substring(1).slice(0,-1);
|
||||
case 3: return String(/Wed/).substring(1).slice(0,-1);
|
||||
case 4: return String(/Thu/).substring(1).slice(0,-1);
|
||||
case 5: return String(/Fri/).substring(1).slice(0,-1);
|
||||
case 6: return String(/Sat/).substring(1).slice(0,-1);
|
||||
}
|
||||
}"""
|
||||
break
|
||||
case "Monthly":
|
||||
graphDateFunction = """ function(date) {
|
||||
var month = date.getMonth();
|
||||
switch(month) {
|
||||
case 0: return String(/Jan/).substring(1).slice(0,-1);
|
||||
case 1: return String(/Feb/).substring(1).slice(0,-1);
|
||||
case 2: return String(/Mar/).substring(1).slice(0,-1);
|
||||
case 3: return String(/Apr/).substring(1).slice(0,-1);
|
||||
case 4: return String(/May/).substring(1).slice(0,-1);
|
||||
case 5: return String(/Jun/).substring(1).slice(0,-1);
|
||||
case 6: return String(/Jul/).substring(1).slice(0,-1);
|
||||
case 7: return String(/Aug/).substring(1).slice(0,-1);
|
||||
case 8: return String(/Sep/).substring(1).slice(0,-1);
|
||||
case 9: return String(/Oct/).substring(1).slice(0,-1);
|
||||
case 10: return String(/Nov/).substring(1).slice(0,-1);
|
||||
case 11: return String(/Dec/).substring(1).slice(0,-1);
|
||||
}
|
||||
}"""
|
||||
break
|
||||
case "Annually":
|
||||
graphDateFunction = """
|
||||
function(date) {
|
||||
return.getFullYear();
|
||||
}
|
||||
"""
|
||||
break
|
||||
}
|
||||
groovy.json.StringEscapeUtils.escapeJavaScript(graphDateFunction)
|
||||
}
|
||||
|
||||
private jsEscapeString(str = "") {
|
||||
"String(/${str}/).substring(1).slice(0,-1);"
|
||||
}
|
||||
|
||||
private PLATFORM_fetchGraphData(attribute) {
|
||||
|
||||
log.debug "PLATFORM_fetchGraphData(${attribute})"
|
||||
|
||||
/*
|
||||
[
|
||||
[
|
||||
dateString: "2014-12-1",
|
||||
unixTime: 1421931600000,
|
||||
min: 0,
|
||||
max: 10,
|
||||
average: 5
|
||||
],
|
||||
...
|
||||
]
|
||||
*/
|
||||
|
||||
def attributeBucket = state["measure.${attribute}"] ?: [:]
|
||||
def dateType = selectedGraphPrecision()
|
||||
attributeBucket[dateType]
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// PLATFORM DATA STORAGE
|
||||
// ========================================================
|
||||
|
||||
private PLATFORM_graphPrecisionOptions() { ["Live", "Hourly", "Daily", "Monthly", "Annually"] }
|
||||
|
||||
private PLATFORM_storeData(attribute, value) {
|
||||
PLATFORM_graphPrecisionOptions().each { dateType ->
|
||||
PLATFORM_addDataToBucket(attribute, value, dateType)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
[
|
||||
Hourly: [
|
||||
[
|
||||
dateString: "2014-12-1",
|
||||
unixTime: 1421931600000,
|
||||
min: 0,
|
||||
max: 10,
|
||||
average: 5
|
||||
],
|
||||
...
|
||||
],
|
||||
...
|
||||
]
|
||||
*/
|
||||
|
||||
private PLATFORM_addDataToBucket(attribute, value, dateType) {
|
||||
|
||||
def numberValue = value.toBigDecimal()
|
||||
|
||||
def attributeKey = "measure.${attribute}"
|
||||
def attributeBucket = state[attributeKey] ?: [:]
|
||||
|
||||
def dateTypeBucket = attributeBucket[dateType] ?: []
|
||||
|
||||
def now = new Date()
|
||||
def itemDateString = now.format("PLATFORM_get${dateType}Format"())
|
||||
def item = dateTypeBucket.find { it.dateString == itemDateString }
|
||||
|
||||
if (!item) {
|
||||
// no entry for this data point yet, fill with initial values
|
||||
item = [:]
|
||||
item.average = numberValue
|
||||
item.runningSum = numberValue
|
||||
item.runningCount = 1
|
||||
item.min = numberValue
|
||||
item.max = numberValue
|
||||
item.unixTime = now.getTime()
|
||||
item.dateString = itemDateString
|
||||
|
||||
// add the new data point
|
||||
dateTypeBucket << item
|
||||
|
||||
// clear out old data points
|
||||
def old = PLATFORM_getOldDateString(dateType)
|
||||
if (old) { // annual data never gets cleared
|
||||
dateTypeBucket.findAll { it.unixTime < old }.each { dateTypeBucket.remove(it) }
|
||||
}
|
||||
|
||||
// limit the size of the bucket. Live data can stack up fast
|
||||
def sizeLimit = 25
|
||||
if (dateTypeBucket.size() > sizeLimit) {
|
||||
dateTypeBucket = dateTypeBucket[-sizeLimit..-1]
|
||||
}
|
||||
|
||||
} else {
|
||||
//re-calculate average/min/max for this bucket
|
||||
item.runningSum = (item.runningSum.toBigDecimal()) + numberValue
|
||||
item.runningCount = item.runningCount.toInteger() + 1
|
||||
item.average = item.runningSum.toBigDecimal() / item.runningCount.toInteger()
|
||||
|
||||
if (item.min == null) {
|
||||
item.min = numberValue
|
||||
} else if (numberValue < item.min.toBigDecimal()) {
|
||||
item.min = numberValue
|
||||
}
|
||||
if (item.max == null) {
|
||||
item.max = numberValue
|
||||
} else if (numberValue > item.max.toBigDecimal()) {
|
||||
item.max = numberValue
|
||||
}
|
||||
}
|
||||
|
||||
attributeBucket[dateType] = dateTypeBucket
|
||||
state[attributeKey] = attributeBucket
|
||||
}
|
||||
|
||||
private PLATFORM_getOldDateString(dateType) {
|
||||
def now = new Date()
|
||||
def date
|
||||
switch (dateType) {
|
||||
case "Live":
|
||||
date = now.getTime() - 60 * 60 * 1000 // 1h * 60m * 60s * 1000ms // 1 hour
|
||||
break
|
||||
case "Hourly":
|
||||
date = (now - 1).getTime()
|
||||
break
|
||||
case "Daily":
|
||||
date = (now - 10).getTime()
|
||||
break
|
||||
case "Monthly":
|
||||
date = (now - 30).getTime()
|
||||
break
|
||||
case "Annually":
|
||||
break
|
||||
}
|
||||
date
|
||||
}
|
||||
|
||||
private PLATFORM_getLiveFormat() { "HH:mm:ss" }
|
||||
|
||||
private PLATFORM_getHourlyFormat() { "yyyy-MM-dd'T'HH" }
|
||||
|
||||
private PLATFORM_getDailyFormat() { "yyyy-MM-dd" }
|
||||
|
||||
private PLATFORM_getMonthlyFormat() { "yyyy-MM" }
|
||||
|
||||
private PLATFORM_getAnnuallyFormat() { "yyyy" }
|
||||
|
||||
// ========================================================
|
||||
// PLATFORM GRAPH DATA MIGRATION
|
||||
// ========================================================
|
||||
|
||||
private PLATFORM_migrateGraphDataIfNeeded() {
|
||||
if (!state.hasMigratedOldGraphData) {
|
||||
def acceptableKeys = PLATFORM_graphPrecisionOptions()
|
||||
def needsMigration = state.findAll { k, v -> v.keySet().findAll { !acceptableKeys.contains(it) } }.keySet()
|
||||
needsMigration.each { PLATFORM_migrateGraphData(it) }
|
||||
state.hasMigratedOldGraphData = true
|
||||
}
|
||||
}
|
||||
|
||||
private PLATFORM_migrateGraphData(attribute) {
|
||||
|
||||
log.trace "about to migrate ${attribute}"
|
||||
|
||||
def attributeBucket = state[attribute] ?: [:]
|
||||
def migratedAttributeBucket = [:]
|
||||
|
||||
attributeBucket.findAll { k, v -> !PLATFORM_graphPrecisionOptions().contains(k) }.each { oldDateString, oldItem ->
|
||||
|
||||
def dateType = oldDateString.contains('T') ? "Hourly" : PLATFORM_graphPrecisionOptions().find {
|
||||
"PLATFORM_get${it}Format"().size() == oldDateString.size()
|
||||
}
|
||||
|
||||
def dateTypeFormat = "PLATFORM_get${dateType}Format"()
|
||||
|
||||
def newBucket = attributeBucket[dateType] ?: []
|
||||
/*
|
||||
def existingNewItem = newBucket.find { it.dateString == oldDateString }
|
||||
if (existingNewItem) {
|
||||
newBucket.remove(existingNewItem)
|
||||
}
|
||||
*/
|
||||
|
||||
def newItem = [
|
||||
min : oldItem.min,
|
||||
max : oldItem.max,
|
||||
average : oldItem.average,
|
||||
runningSum : oldItem.runningSum,
|
||||
runningCount: oldItem.runningCount,
|
||||
dateString : oldDateString,
|
||||
unixTime : new Date().parse(dateTypeFormat, oldDateString).getTime()
|
||||
]
|
||||
|
||||
newBucket << newItem
|
||||
migratedAttributeBucket[dateType] = newBucket
|
||||
}
|
||||
|
||||
state[attribute] = migratedAttributeBucket
|
||||
renderGraph(attribute: params.attribute, xValues: xValues, yValues: yValues, focus: "Total", label: "Watts")
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,887 @@
|
||||
/**
|
||||
* Fidure Thermostat, Based on ZigBee thermostat (SmartThings)
|
||||
*
|
||||
* Author: Fidure
|
||||
* Date: 2014-12-13
|
||||
* Updated: 2015-08-26
|
||||
*/
|
||||
metadata {
|
||||
// Automatically generated. Make future change here.
|
||||
definition (name: "Fidure Thermostat", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
capability "Actuator"
|
||||
capability "Temperature Measurement"
|
||||
capability "Thermostat"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Polling"
|
||||
|
||||
attribute "displayTemperature","number"
|
||||
attribute "displaySetpoint", "string"
|
||||
command "raiseSetpoint"
|
||||
command "lowerSetpoint"
|
||||
attribute "upButtonState", "string"
|
||||
attribute "downButtonState", "string"
|
||||
|
||||
attribute "runningMode", "string"
|
||||
attribute "lockLevel", "string"
|
||||
|
||||
command "setThermostatTime"
|
||||
command "lock"
|
||||
|
||||
attribute "prorgammingOperation", "number"
|
||||
attribute "prorgammingOperationDisplay", "string"
|
||||
command "Program"
|
||||
|
||||
attribute "setpointHold", "string"
|
||||
attribute "setpointHoldDisplay", "string"
|
||||
command "Hold"
|
||||
attribute "holdExpiary", "string"
|
||||
|
||||
attribute "lastTimeSync", "string"
|
||||
|
||||
attribute "thermostatOperatingState", "string"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0201,0204,0B05", outClusters: "000A, 0019"
|
||||
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator { }
|
||||
// pref
|
||||
preferences {
|
||||
|
||||
input ("hold_time", "enum", title: "Default Hold Time in Hours",
|
||||
description: "Default Hold Duration in hours",
|
||||
range: "1..24", options: ["No Hold", "2 Hours", "4 Hours", "8 Hours", "12 Hours", "1 Day"],
|
||||
displayDuringSetup: false)
|
||||
input ("sync_clock", "boolean", title: "Synchronize Thermostat Clock Automatically?", options: ["Yes","No"])
|
||||
input ("lock_level", "enum", title: "Thermostat Screen Lock Level", options: ["Full","Mode Only", "Setpoint"])
|
||||
}
|
||||
|
||||
tiles {
|
||||
valueTile("temperature", "displayTemperature", width: 2, height: 2) {
|
||||
state("temperature", label:'${currentValue}°', unit:"F",
|
||||
backgroundColors:[
|
||||
[value: 0, color: "#153591"],
|
||||
[value: 7, color: "#1e9cbb"],
|
||||
[value: 15, color: "#90d2a7"],
|
||||
[value: 23, color: "#44b621"],
|
||||
[value: 29, color: "#f1d801"],
|
||||
[value: 35, color: "#d04e00"],
|
||||
[value: 36, color: "#bc2323"],
|
||||
// fahrenheit range
|
||||
[value: 37, 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"]
|
||||
]
|
||||
)
|
||||
}
|
||||
standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
|
||||
state "off", action:"thermostat.setThermostatMode", icon:"st.thermostat.heating-cooling-off"
|
||||
state "cool", action:"thermostat.setThermostatMode", icon:"st.thermostat.cool"
|
||||
state "heat", action:"thermostat.setThermostatMode", icon:"st.thermostat.heat"
|
||||
state "auto", action:"thermostat.setThermostatMode", icon:"st.thermostat.auto"
|
||||
}
|
||||
|
||||
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
|
||||
state "fanAuto", label:'${name}', action:"thermostat.setThermostatFanMode"
|
||||
state "fanOn", label:'${name}', action:"thermostat.setThermostatFanMode"
|
||||
}
|
||||
|
||||
standardTile("hvacStatus", "thermostatOperatingState", inactiveLabel: false, decoration: "flat") {
|
||||
state "Resting", label: 'Resting'
|
||||
state "Heating", icon:"st.thermostat.heating"
|
||||
state "Cooling", icon:"st.thermostat.cooling"
|
||||
}
|
||||
|
||||
|
||||
standardTile("lock", "lockLevel", inactiveLabel: false, decoration: "flat") {
|
||||
state "Unlocked", action:"lock", label:'${name}'
|
||||
state "Mode Only", action:"lock", label:'${name}'
|
||||
state "Setpoint", action:"lock", label:'${name}'
|
||||
state "Full", action:"lock", label:'${name}'
|
||||
}
|
||||
|
||||
controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false, range: "$min..$max") {
|
||||
state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#d04e00"
|
||||
}
|
||||
valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||
state "heat", label:'${currentValue}° heat', unit:"F", backgroundColor:"#ffffff"
|
||||
}
|
||||
controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false, range: "$min..$max") {
|
||||
state "setCoolingSetpoint", action:"thermostat.setCoolingSetpoint", backgroundColor: "#1e9cbb"
|
||||
}
|
||||
valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||
state "cool", label:'${currentValue}° cool', unit:"F", backgroundColor:"#ffffff"
|
||||
}
|
||||
standardTile("refresh", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
|
||||
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||
}
|
||||
|
||||
valueTile("scheduleText", "prorgammingOperation", inactiveLabel: false, decoration: "flat", width: 2) {
|
||||
state "default", label: 'Schedule'
|
||||
}
|
||||
valueTile("schedule", "prorgammingOperationDisplay", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", action:"Program", label: '${currentValue}'
|
||||
}
|
||||
|
||||
valueTile("hold", "setpointHoldDisplay", inactiveLabel: false, decoration: "flat", width: 3) {
|
||||
state "setpointHold", action:"Hold", label: '${currentValue}'
|
||||
}
|
||||
|
||||
valueTile("setpoint", "displaySetpoint", width: 2, height: 2)
|
||||
{
|
||||
state("displaySetpoint", label: '${currentValue}°',
|
||||
backgroundColor: "#919191")
|
||||
}
|
||||
|
||||
standardTile("upButton", "upButtonState", decoration: "flat", inactiveLabel: false) {
|
||||
state "normal", action:"raiseSetpoint", backgroundColor:"#919191", icon:"st.thermostat.thermostat-up"
|
||||
state "pressed", action:"raiseSetpoint", backgroundColor:"#ff0000", icon:"st.thermostat.thermostat-up"
|
||||
}
|
||||
standardTile("downButton", "downButtonState", decoration: "flat", inactiveLabel: false) {
|
||||
state "normal", action:"lowerSetpoint", backgroundColor:"#919191", icon:"st.thermostat.thermostat-down"
|
||||
state "pressed", action:"lowerSetpoint", backgroundColor:"#ff9191", icon:"st.thermostat.thermostat-down"
|
||||
}
|
||||
|
||||
|
||||
main "temperature"
|
||||
details([ "temperature", "mode", "hvacStatus","setpoint","upButton","downButton","scheduleText", "schedule", "hold",
|
||||
"heatSliderControl", "heatingSetpoint","coolSliderControl", "coolingSetpoint", "lock", "refresh", "configure"])
|
||||
}
|
||||
}
|
||||
|
||||
def getMin() {
|
||||
try
|
||||
{
|
||||
if (getTemperatureScale() == "C")
|
||||
return 10
|
||||
else
|
||||
return 50
|
||||
} catch (all)
|
||||
{
|
||||
return 10
|
||||
}
|
||||
}
|
||||
|
||||
def getMax() {
|
||||
try {
|
||||
if (getTemperatureScale() == "C")
|
||||
return 30
|
||||
else
|
||||
return 86
|
||||
} catch (all)
|
||||
{
|
||||
return 86
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parse description $description"
|
||||
def result = []
|
||||
|
||||
if (description?.startsWith("read attr -")) {
|
||||
|
||||
//TODO: Parse RAW strings for multiple attributes
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
log.debug "Desc Map: $descMap"
|
||||
for ( atMap in descMap.attrs)
|
||||
{
|
||||
def map = [:]
|
||||
|
||||
if (descMap.cluster == "0201")
|
||||
{
|
||||
//log.trace "attribute: ${atMap.attrId} "
|
||||
switch(atMap.attrId.toLowerCase())
|
||||
{
|
||||
case "0000":
|
||||
map.name = "temperature"
|
||||
map.value = getTemperature(atMap.value)
|
||||
result += createEvent("name":"displayTemperature", "value": getDisplayTemperature(atMap.value))
|
||||
break;
|
||||
case "0005":
|
||||
//log.debug "hex time: ${descMap.value}"
|
||||
if (atMap.encoding == "23")
|
||||
{
|
||||
map.name = "holdExpiary"
|
||||
map.value = "${convertToTime(atMap.value).getTime()}"
|
||||
//log.trace "HOLD EXPIRY: ${atMap.value} is ${map.value}"
|
||||
updateHoldLabel("HoldExp", "${map.value}")
|
||||
}
|
||||
break;
|
||||
case "0011":
|
||||
map.name = "coolingSetpoint"
|
||||
map.value = getDisplayTemperature(atMap.value)
|
||||
updateSetpoint(map.name,map.value)
|
||||
break;
|
||||
case "0012":
|
||||
map.name = "heatingSetpoint"
|
||||
map.value = getDisplayTemperature(atMap.value)
|
||||
updateSetpoint(map.name,map.value)
|
||||
break;
|
||||
case "001c":
|
||||
map.name = "thermostatMode"
|
||||
map.value = getModeMap()[atMap.value]
|
||||
updateSetpoint(map.name,map.value)
|
||||
break;
|
||||
case "001e": //running mode enum8
|
||||
map.name = "runningMode"
|
||||
map.value = getModeMap()[atMap.value]
|
||||
updateSetpoint(map.name,map.value)
|
||||
break;
|
||||
case "0023": // setpoint hold enum8
|
||||
map.name = "setpointHold"
|
||||
map.value = getHoldMap()[atMap.value]
|
||||
updateHoldLabel("Hold", map.value)
|
||||
break;
|
||||
case "0024": // hold duration int16u
|
||||
map.name = "setpointHoldDuration"
|
||||
map.value = Integer.parseInt("${atMap.value}", 16)
|
||||
|
||||
break;
|
||||
case "0025": // thermostat programming operation bitmap8
|
||||
map.name = "prorgammingOperation"
|
||||
def val = getProgrammingMap()[Integer.parseInt("${atMap.value}", 16) & 0x01]
|
||||
result += createEvent("name":"prorgammingOperationDisplay", "value": val)
|
||||
map.value = atMap.value
|
||||
break;
|
||||
case "0029":
|
||||
// relay state
|
||||
map.name = "thermostatOperatingState"
|
||||
map.value = getThermostatOperatingState(atMap.value)
|
||||
break;
|
||||
}
|
||||
} else if (descMap.cluster == "0204")
|
||||
{
|
||||
if (atMap.attrId == "0001")
|
||||
{
|
||||
map.name = "lockLevel"
|
||||
map.value = getLockMap()[atMap.value]
|
||||
}
|
||||
}
|
||||
|
||||
if (map) {
|
||||
result += createEvent(map)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.debug "Parse returned $result"
|
||||
return result
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
def map = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
|
||||
def attrId = map.get('attrId')
|
||||
def encoding = map.get('encoding')
|
||||
def value = map.get('value')
|
||||
def result = map.get('result')
|
||||
def list = [];
|
||||
|
||||
if (getDataLengthByType(map.get('encoding')) < map.get('value').length()) {
|
||||
def raw = map.get('raw')
|
||||
|
||||
def size = Long.parseLong(''+ map.get('size'), 16)
|
||||
def index = 12;
|
||||
def len
|
||||
|
||||
//log.trace "processing multi attributes"
|
||||
while((index-12) < size) {
|
||||
attrId = flipHexStringEndianness(raw[index..(index+3)])
|
||||
index+= 4;
|
||||
if (result == "success")
|
||||
index+=2;
|
||||
encoding = raw[index..(index+1)]
|
||||
index+= 2;
|
||||
len =getDataLengthByType(encoding)
|
||||
value = flipHexStringEndianness(raw[index..(index+len-1)])
|
||||
index+=len;
|
||||
list += ['attrId': "$attrId", 'encoding':"$encoding", 'value': "$value"]
|
||||
}
|
||||
}
|
||||
else
|
||||
list += ['attrId': "$attrId", 'encoding': "$encoding", 'value': "$value"]
|
||||
|
||||
map.remove('value')
|
||||
map.remove('encoding')
|
||||
map.remove('attrId')
|
||||
map += ['attrs' : list ]
|
||||
}
|
||||
|
||||
def flipHexStringEndianness(s)
|
||||
{
|
||||
s = s.reverse()
|
||||
def sb = new StringBuilder()
|
||||
for (int i=0; i < s.length() -1; i+=2)
|
||||
sb.append(s.charAt(i+1)).append(s.charAt(i))
|
||||
sb
|
||||
}
|
||||
|
||||
def getDataLengthByType(t)
|
||||
{
|
||||
// number of bytes in each static data type
|
||||
def map = ["08":1, "09":2, "0a":3, "0b":4, "0c":5, "0d":6, "0e":7, "0f":8, "10":1, "18":1, "19":2, "1a":3, "1b":4,
|
||||
"1c":5,"1d":6, "1e":7, "1f":8, "20":1, "21":2, "22":3, "23":4, "24":5, "25":6, "26":7, "27":8, "28":1, "29":2,
|
||||
"2a":3, "2b":4, "2c":5, "2d":6, "2e":7, "2f":8, "30":1, "31":2, "38":2, "39":4, "40":8, "e0":4, "e1":4, "e2":4,
|
||||
"e8":2, "e9":2, "ea":4, "f0":8, "f1":16]
|
||||
|
||||
// return number of hex chars
|
||||
return map.get(t) * 2
|
||||
}
|
||||
|
||||
|
||||
def getProgrammingMap() { [
|
||||
0:"Off",
|
||||
1:"On"
|
||||
]}
|
||||
|
||||
def getModeMap() { [
|
||||
"00":"off",
|
||||
"01":"auto",
|
||||
"03":"cool",
|
||||
"04":"heat"
|
||||
]}
|
||||
|
||||
def getFanModeMap() { [
|
||||
"04":"fanOn",
|
||||
"05":"fanAuto"
|
||||
]}
|
||||
|
||||
def getHoldMap()
|
||||
{[
|
||||
"00":"Off",
|
||||
"01":"On"
|
||||
]}
|
||||
|
||||
|
||||
def updateSetpoint(attrib, val)
|
||||
{
|
||||
def cool = device.currentState("coolingSetpoint")?.value
|
||||
def heat = device.currentState("heatingSetpoint")?.value
|
||||
def runningMode = device.currentState("runningMode")?.value
|
||||
def mode = device.currentState("thermostatMode")?.value
|
||||
|
||||
def value = '--';
|
||||
|
||||
|
||||
if ("heat" == mode && heat != null)
|
||||
value = heat;
|
||||
else if ("cool" == mode && cool != null)
|
||||
value = cool;
|
||||
else if ("auto" == mode && runningMode == "cool" && cool != null)
|
||||
value = cool;
|
||||
else if ("auto" == mode && runningMode == "heat" && heat != null)
|
||||
value = heat;
|
||||
|
||||
sendEvent("name":"displaySetpoint", "value": value)
|
||||
}
|
||||
|
||||
def raiseSetpoint()
|
||||
{
|
||||
sendEvent("name":"upButtonState", "value": "pressed")
|
||||
sendEvent("name":"upButtonState", "value": "normal")
|
||||
adjustSetpoint(5)
|
||||
}
|
||||
|
||||
def lowerSetpoint()
|
||||
{
|
||||
sendEvent("name":"downButtonState", "value": "pressed")
|
||||
sendEvent("name":"downButtonState", "value": "normal")
|
||||
adjustSetpoint(-5)
|
||||
}
|
||||
|
||||
def adjustSetpoint(value)
|
||||
{
|
||||
def runningMode = device.currentState("runningMode")?.value
|
||||
def mode = device.currentState("thermostatMode")?.value
|
||||
|
||||
//default to both heat and cool
|
||||
def modeData = 0x02
|
||||
|
||||
if ("heat" == mode || "heat" == runningMode)
|
||||
modeData = "00"
|
||||
else if ("cool" == mode || "cool" == runningMode)
|
||||
modeData = "01"
|
||||
|
||||
def amountData = String.format("%02X", value)[-2..-1]
|
||||
|
||||
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x201 0 {" + modeData + " " + amountData + "}"
|
||||
|
||||
}
|
||||
|
||||
|
||||
def getDisplayTemperature(value)
|
||||
{
|
||||
def t = Integer.parseInt("$value", 16);
|
||||
|
||||
|
||||
if (getTemperatureScale() == "C") {
|
||||
t = (((t + 4) / 10) as Integer) / 10;
|
||||
} else {
|
||||
t = ((10 *celsiusToFahrenheit(t/100)) as Integer)/ 10;
|
||||
}
|
||||
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
def updateHoldLabel(attr, value)
|
||||
{
|
||||
def currentHold = (device?.currentState("setpointHold")?.value)?: "..."
|
||||
|
||||
def holdExp = device?.currentState("holdExpiary")?.value
|
||||
holdExp = holdExp?: "${(new Date()).getTime()}"
|
||||
|
||||
if ("Hold" == attr)
|
||||
{
|
||||
currentHold = value
|
||||
}
|
||||
|
||||
if ("HoldExp" == attr)
|
||||
{
|
||||
holdExp = value
|
||||
}
|
||||
boolean past = ( (new Date(holdExp.toLong()).getTime()) < (new Date().getTime()))
|
||||
|
||||
if ("HoldExp" == attr)
|
||||
{
|
||||
if (!past)
|
||||
currentHold = "On"
|
||||
else
|
||||
currentHold = "Off"
|
||||
}
|
||||
|
||||
def holdString = (currentHold == "On")?
|
||||
( (past)? "Is On" : "Ends ${compareWithNow(holdExp.toLong())}") :
|
||||
((currentHold == "Off")? " is Off" : " ...")
|
||||
|
||||
sendEvent("name":"setpointHoldDisplay", "value": "Hold ${holdString}")
|
||||
}
|
||||
|
||||
def getSetPointHoldDuration()
|
||||
{
|
||||
def holdTime = 0
|
||||
|
||||
if (settings.hold_time?.contains("Hours"))
|
||||
{
|
||||
holdTime = Integer.parseInt(settings.hold_time[0..1].trim())
|
||||
}
|
||||
else if (settings.hold_time?.contains("Day"))
|
||||
{
|
||||
holdTime = Integer.parseInt(settings.hold_time[0..1].trim()) * 24
|
||||
}
|
||||
|
||||
def currentHoldDuration = device.currentState("setpointHoldDuration")?.value
|
||||
|
||||
|
||||
if (Short.parseShort('0'+ (currentHoldDuration?: 0)) != (holdTime * 60))
|
||||
{
|
||||
[
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x24 0x21 {" +
|
||||
String.format("%04X", ((holdTime * 60) as Short)) // switch to zigbee endian
|
||||
|
||||
+ "}", "delay 100",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x201 0x24", "delay 200",
|
||||
]
|
||||
|
||||
} else
|
||||
{
|
||||
[]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def Hold()
|
||||
{
|
||||
def currentHold = device.currentState("setpointHold")?.value
|
||||
|
||||
def next = (currentHold == "On") ? "00" : "01"
|
||||
def nextHold = getHoldMap()[next]
|
||||
|
||||
sendEvent("name":"setpointHold", "value":nextHold)
|
||||
|
||||
// set the duration first if it's changed
|
||||
|
||||
[
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x23 0x30 {$next}", "delay 100" ,
|
||||
|
||||
"raw 0x201 {04 21 11 00 00 05 00 }","delay 200", // hold expiry time
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
] + getSetPointHoldDuration()
|
||||
}
|
||||
|
||||
def compareWithNow(d)
|
||||
{
|
||||
long mins = (new Date(d)).getTime() - (new Date()).getTime()
|
||||
|
||||
mins /= 1000 * 60;
|
||||
|
||||
log.trace "mins: ${mins}"
|
||||
|
||||
boolean past = (mins < 0)
|
||||
def ret = (past)? "" : "in "
|
||||
|
||||
if (past)
|
||||
mins *= -1;
|
||||
|
||||
float t = 0;
|
||||
// minutes
|
||||
if (mins < 60)
|
||||
{
|
||||
ret += (mins as Integer) + " min" + ((mins > 1)? 's' : '')
|
||||
}else if (mins < 1440)
|
||||
{
|
||||
t = ( Math.round((14 + mins)/30) as Integer) / 2
|
||||
ret += t + " hr" + ((t > 1)? 's' : '')
|
||||
} else
|
||||
{
|
||||
t = (Math.round((359 + mins)/720) as Integer) / 2
|
||||
ret += t + " day" + ((t > 1)? 's' : '')
|
||||
}
|
||||
ret += (past)? " ago": ""
|
||||
|
||||
log.trace "ret: ${ret}"
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
def convertToTime(data)
|
||||
{
|
||||
def time = Integer.parseInt("$data", 16) as long;
|
||||
time *= 1000;
|
||||
time += 946684800000; // 481418694
|
||||
time -= location.timeZone.getRawOffset() + location.timeZone.getDSTSavings();
|
||||
|
||||
def d = new Date(time);
|
||||
|
||||
//log.trace "converted $data to Time $d"
|
||||
return d;
|
||||
}
|
||||
|
||||
def Program()
|
||||
{
|
||||
def currentSched = device.currentState("prorgammingOperation")?.value
|
||||
|
||||
def next = Integer.parseInt(currentSched?: "00", 16);
|
||||
if ( (next & 0x01) == 0x01)
|
||||
next = next & 0xfe;
|
||||
else
|
||||
next = next | 0x01;
|
||||
|
||||
def nextSched = getProgrammingMap()[next & 0x01]
|
||||
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x25 0x18 {$next}"
|
||||
|
||||
}
|
||||
|
||||
|
||||
def getThermostatOperatingState(value)
|
||||
{
|
||||
String[] m = [ "heating", "cooling", "fan", "Heat2", "Cool2", "Fan2", "Fan3"]
|
||||
String desc = 'idle'
|
||||
value = Integer.parseInt(''+value, 16)
|
||||
|
||||
// only check for 1-stage for A1730
|
||||
for ( i in 0..2 ) {
|
||||
if (value & 1 << i)
|
||||
desc = m[i]
|
||||
}
|
||||
|
||||
desc
|
||||
}
|
||||
|
||||
def checkLastTimeSync(delay)
|
||||
{
|
||||
def lastSync = device.currentState("lastTimeSync")?.value
|
||||
if (!lastSync)
|
||||
lastSync = "${new Date(0)}"
|
||||
|
||||
if (settings.sync_clock ?: false && lastSync != new Date(0))
|
||||
{
|
||||
sendEvent("name":"lastTimeSync", "value":"${new Date(0)}")
|
||||
}
|
||||
|
||||
|
||||
|
||||
long duration = (new Date()).getTime() - (new Date(lastSync)).getTime()
|
||||
|
||||
// log.debug "check Time: $lastSync duration: ${duration} settings.sync_clock: ${settings.sync_clock}"
|
||||
if (duration > 86400000)
|
||||
{
|
||||
sendEvent("name":"lastTimeSync", "value":"${new Date()}")
|
||||
return setThermostatTime()
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
def readAttributesCommand(cluster, attribList)
|
||||
{
|
||||
def attrString = ''
|
||||
|
||||
for ( val in attribList ) {
|
||||
attrString += ' ' + String.format("%02X %02X", val & 0xff , (val >> 8) & 0xff)
|
||||
}
|
||||
|
||||
//log.trace "list: " + attrString
|
||||
|
||||
["raw "+ cluster + " {00 00 00 $attrString}","delay 100",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 100",
|
||||
]
|
||||
}
|
||||
|
||||
def refresh()
|
||||
{
|
||||
log.debug "refresh called"
|
||||
// log.trace "list: " + readAttributesCommand(0x201, [0x1C,0x1E,0x23])
|
||||
|
||||
readAttributesCommand(0x201, [0x00,0x11,0x12]) +
|
||||
readAttributesCommand(0x201, [0x1C,0x1E,0x23]) +
|
||||
readAttributesCommand(0x201, [0x24,0x25,0x29]) +
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x204 0x01", "delay 200", // lock status
|
||||
"raw 0x201 {04 21 11 00 00 05 00 }" , "delay 500", // hold expiary
|
||||
"send 0x${device.deviceNetworkId} 1 1" , "delay 1500",
|
||||
] + checkLastTimeSync(2000)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
def poll() {
|
||||
log.trace "poll called"
|
||||
refresh()
|
||||
}
|
||||
|
||||
def getTemperature(value) {
|
||||
def celsius = Integer.parseInt("$value", 16) / 100
|
||||
|
||||
if(getTemperatureScale() == "C"){
|
||||
return celsius as Integer
|
||||
} else {
|
||||
return celsiusToFahrenheit(celsius) as Integer
|
||||
}
|
||||
}
|
||||
|
||||
def setHeatingSetpoint(degrees) {
|
||||
def temperatureScale = getTemperatureScale()
|
||||
|
||||
def degreesInteger = degrees as Integer
|
||||
sendEvent("name":"heatingSetpoint", "value":degreesInteger)
|
||||
|
||||
def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius*100) + "}"
|
||||
|
||||
}
|
||||
|
||||
def setCoolingSetpoint(degrees) {
|
||||
def degreesInteger = degrees as Integer
|
||||
sendEvent("name":"coolingSetpoint", "value":degreesInteger)
|
||||
def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius*100) + "}"
|
||||
|
||||
}
|
||||
|
||||
def modes() {
|
||||
["off", "heat", "cool"]
|
||||
}
|
||||
|
||||
def setThermostatFanMode() {
|
||||
def currentFanMode = device.currentState("thermostatFanMode")?.value
|
||||
//log.debug "switching fan from current mode: $currentFanMode"
|
||||
def returnCommand
|
||||
|
||||
switch (currentFanMode) {
|
||||
case "fanAuto":
|
||||
returnCommand = fanOn()
|
||||
break
|
||||
case "fanOn":
|
||||
returnCommand = fanAuto()
|
||||
break
|
||||
}
|
||||
if(!currentFanMode) { returnCommand = fanAuto() }
|
||||
returnCommand
|
||||
}
|
||||
|
||||
def setThermostatMode() {
|
||||
def currentMode = device.currentState("thermostatMode")?.value
|
||||
def modeOrder = modes()
|
||||
def index = modeOrder.indexOf(currentMode)
|
||||
def next = index >= 0 && index < modeOrder.size() - 1 ? modeOrder[index + 1] : modeOrder[0]
|
||||
|
||||
setThermostatMode(next)
|
||||
}
|
||||
|
||||
def setThermostatMode(String next) {
|
||||
def val = (getModeMap().find { it.value == next }?.key)?: "00"
|
||||
|
||||
// log.trace "mode changing to $next sending value: $val"
|
||||
|
||||
sendEvent("name":"thermostatMode", "value":"$next")
|
||||
["st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {$val}"] +
|
||||
refresh()
|
||||
}
|
||||
|
||||
def setThermostatFanMode(String value) {
|
||||
log.debug "setThermostatFanMode({$value})"
|
||||
"$value"()
|
||||
}
|
||||
|
||||
def off() {
|
||||
setThermostatMode("off")
|
||||
}
|
||||
|
||||
def cool() {
|
||||
setThermostatMode("cool")}
|
||||
|
||||
def heat() {
|
||||
setThermostatMode("heat")
|
||||
}
|
||||
|
||||
def auto() {
|
||||
setThermostatMode("auto")
|
||||
}
|
||||
|
||||
def on() {
|
||||
fanOn()
|
||||
}
|
||||
|
||||
def fanOn() {
|
||||
sendEvent("name":"thermostatFanMode", "value":"fanOn")
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {04}"
|
||||
}
|
||||
|
||||
|
||||
def fanAuto() {
|
||||
sendEvent("name":"thermostatFanMode", "value":"fanAuto")
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {05}"
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
def lastSync = device.currentState("lastTimeSync")?.value
|
||||
if ((settings.sync_clock ?: false) == false)
|
||||
{
|
||||
log.debug "resetting last sync time. Used to be: $lastSync"
|
||||
sendEvent("name":"lastTimeSync", "value":"${new Date(0)}")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def getLockMap()
|
||||
{[
|
||||
"00":"Unlocked",
|
||||
"01":"Mode Only",
|
||||
"02":"Setpoint",
|
||||
"03":"Full",
|
||||
"04":"Full",
|
||||
"05":"Full",
|
||||
|
||||
]}
|
||||
def lock()
|
||||
{
|
||||
|
||||
def currentLock = device.currentState("lockLevel")?.value
|
||||
def val = getLockMap().find { it.value == currentLock }?.key
|
||||
|
||||
|
||||
|
||||
//log.debug "current lock is: ${val}"
|
||||
|
||||
if (val == "00")
|
||||
val = getLockMap().find { it.value == (settings.lock_level ?: "Full") }?.key
|
||||
else
|
||||
val = "00"
|
||||
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x204 0x01"
|
||||
|
||||
}
|
||||
|
||||
|
||||
def setThermostatTime()
|
||||
{
|
||||
|
||||
if ((settings.sync_clock ?: false))
|
||||
{
|
||||
log.debug "sync time is disabled, leaving"
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
Date date = new Date();
|
||||
String zone = location.timeZone.getRawOffset() + " DST " + location.timeZone.getDSTSavings();
|
||||
|
||||
long millis = date.getTime(); // Millis since Unix epoch
|
||||
millis -= 946684800000; // adjust for ZigBee EPOCH
|
||||
// adjust for time zone and DST offset
|
||||
millis += location.timeZone.getRawOffset() + location.timeZone.getDSTSavings();
|
||||
//convert to seconds
|
||||
millis /= 1000;
|
||||
|
||||
// print to a string for hex capture
|
||||
String s = String.format("%08X", millis);
|
||||
// hex capture for message format
|
||||
String data = " " + s.substring(6, 8) + " " + s.substring(4, 6) + " " + s.substring(2, 4)+ " " + s.substring(0, 2);
|
||||
|
||||
[
|
||||
"raw 0x201 {04 21 11 00 02 0f 00 23 ${data} }",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}"
|
||||
]
|
||||
}
|
||||
|
||||
def configure() {
|
||||
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x201 {${device.zigbeeId}} {}", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0000 0x29 20 300 {19 00}", // report temperature changes over 0.2C
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x001C 0x30 10 305 { }", // mode
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0025 0x18 10 310 { 00 }", // schedule on/off
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x001E 0x30 10 315 { 00 }", // running mode
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0011 0x29 10 320 {32 00}", // cooling setpoint delta: 0.5C (0x3200 in little endian)
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0012 0x29 10 320 {32 00}", // cooling setpoint delta: 0.5C (0x3200 in little endian)
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0029 0x19 10 325 { 00 }", "delay 200", // relay status
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0023 0x30 10 330 { 00 }", // hold
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 1500",
|
||||
|
||||
] + refresh()
|
||||
}
|
||||
|
||||
private hex(value) {
|
||||
new BigInteger(Math.round(value).toString()).toString(16)
|
||||
}
|
||||
|
||||
private getEndpointId()
|
||||
{
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
@@ -19,7 +19,6 @@ metadata {
|
||||
capability "Sensor"
|
||||
|
||||
fingerprint deviceId: "0x1000", inClusters: "0x25,0x72,0x86,0x71,0x22,0x70"
|
||||
fingerprint deviceId: "0x1006", inClusters: "0x25"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -15,19 +15,27 @@ metadata {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
tiles {
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"rich-control"){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#F3C200"
|
||||
}
|
||||
tileAttribute ("serialNumber", key: "SECONDARY_CONTROL") {
|
||||
attributeState "default", label:'SN: ${currentValue}'
|
||||
}
|
||||
}
|
||||
standardTile("icon", "icon", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) {
|
||||
state "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#FFFFFF"
|
||||
}
|
||||
valueTile("serialNumber", "device.serialNumber", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
|
||||
state "default", label:'SN: ${currentValue}'
|
||||
}
|
||||
valueTile("networkAddress", "device.networkAddress", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
|
||||
valueTile("networkAddress", "device.networkAddress", decoration: "flat", height: 2, width: 4, inactiveLabel: false) {
|
||||
state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false
|
||||
}
|
||||
|
||||
main (["icon"])
|
||||
details(["networkAddress","serialNumber"])
|
||||
details(["rich-control", "networkAddress"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +44,6 @@ def parse(description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
def results = []
|
||||
def result = parent.parse(this, description)
|
||||
|
||||
if (result instanceof physicalgraph.device.HubAction){
|
||||
log.trace "HUE BRIDGE HubAction received -- DOES THIS EVER HAPPEN?"
|
||||
results << result
|
||||
@@ -44,32 +51,30 @@ def parse(description) {
|
||||
//do nothing
|
||||
log.trace "HUE BRIDGE was updated"
|
||||
} else {
|
||||
log.trace "HUE BRIDGE, OTHER"
|
||||
def map = description
|
||||
if (description instanceof String) {
|
||||
map = stringToMap(description)
|
||||
}
|
||||
if (map?.name && map?.value) {
|
||||
log.trace "HUE BRIDGE, GENERATING EVENT: $map.name: $map.value"
|
||||
results << createEvent(name: "${map?.name}", value: "${map?.value}")
|
||||
}
|
||||
else {
|
||||
log.trace "HUE BRIDGE, OTHER"
|
||||
results << createEvent(name: "${map.name}", value: "${map.value}")
|
||||
} else {
|
||||
log.trace "Parsing description"
|
||||
def msg = parseLanMessage(description)
|
||||
if (msg.body) {
|
||||
def contentType = msg.headers["Content-Type"]
|
||||
if (contentType?.contains("json")) {
|
||||
def bulbs = new groovy.json.JsonSlurper().parseText(msg.body)
|
||||
if (bulbs.state) {
|
||||
log.warn "NOT PROCESSED: $msg.body"
|
||||
}
|
||||
else {
|
||||
log.debug "HUE BRIDGE, GENERATING BULB LIST EVENT: $bulbs"
|
||||
sendEvent(name: "bulbList", value: device.hub.id, isStateChange: true, data: bulbs, displayed: false)
|
||||
log.info "Bridge response: $msg.body"
|
||||
} else {
|
||||
// Sending Bulbs List to parent"
|
||||
if (parent.state.inBulbDiscovery)
|
||||
log.info parent.bulbListHandler(device.hub.id, msg.body)
|
||||
}
|
||||
}
|
||||
else if (contentType?.contains("xml")) {
|
||||
log.debug "HUE BRIDGE, SWALLOWING BRIDGE DESCRIPTION RESPONSE -- BRIDGE ALREADY PRESENT"
|
||||
log.debug "HUE BRIDGE ALREADY PRESENT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
/**
|
||||
* Hue Bulb
|
||||
*
|
||||
@@ -15,8 +16,8 @@ metadata {
|
||||
capability "Sensor"
|
||||
|
||||
command "setAdjustedColor"
|
||||
command "reset"
|
||||
command "refresh"
|
||||
command "reset"
|
||||
command "refresh"
|
||||
}
|
||||
|
||||
simulator {
|
||||
@@ -49,7 +50,6 @@ metadata {
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "levelSliderControl", "rgbSelector", "refresh", "reset"])
|
||||
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
@@ -68,13 +68,13 @@ def parse(description) {
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def on(transition = "4") {
|
||||
log.trace parent.on(this,transition)
|
||||
def on() {
|
||||
log.trace parent.on(this)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
|
||||
def off(transition = "4") {
|
||||
log.trace parent.off(this,transition)
|
||||
def off() {
|
||||
log.trace parent.off(this)
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
|
||||
@@ -107,9 +107,9 @@ def setHue(percent) {
|
||||
sendEvent(name: "hue", value: percent)
|
||||
}
|
||||
|
||||
def setColor(value,alert = "none",transition = 4) {
|
||||
def setColor(value) {
|
||||
log.debug "setColor: ${value}, $this"
|
||||
parent.setColor(this, value, alert, transition)
|
||||
parent.setColor(this, value)
|
||||
if (value.hue) { sendEvent(name: "hue", value: value.hue)}
|
||||
if (value.saturation) { sendEvent(name: "saturation", value: value.saturation)}
|
||||
if (value.hex) { sendEvent(name: "color", value: value.hex)}
|
||||
|
||||
@@ -19,24 +19,41 @@ metadata {
|
||||
simulator {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"rich-control", type: "lighting", canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel", range:"(0..100)"
|
||||
}
|
||||
tileAttribute ("device.level", key: "SECONDARY_CONTROL") {
|
||||
attributeState "level", label: 'Level ${currentValue}%'
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
state "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
|
||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821"
|
||||
state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff"
|
||||
}
|
||||
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: 2, inactiveLabel: false, range:"(0..100)") {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||
state "level", label: 'Level ${currentValue}%'
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "levelSliderControl", "refresh"])
|
||||
|
||||
main(["switch"])
|
||||
details(["rich-control", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -24,8 +24,8 @@ metadata {
|
||||
command "enrollResponse"
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3041"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3043"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3045"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3043", deviceJoinName: "NYCE Ceiling Motion Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3045", deviceJoinName: "NYCE Curtain Motion Sensor"
|
||||
}
|
||||
|
||||
tiles {
|
||||
|
||||
@@ -24,10 +24,10 @@ metadata {
|
||||
command "enrollResponse"
|
||||
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011"
|
||||
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014"
|
||||
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3014"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor"
|
||||
}
|
||||
|
||||
simulator {
|
||||
|
||||
@@ -22,10 +22,8 @@ metadata {
|
||||
|
||||
command "setAdjustedColor"
|
||||
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenspot RGB"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Gardenspot RGB"
|
||||
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
|
||||
@@ -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"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,16 +280,14 @@ def configure() {
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 1 0x20 0x20 300 0600 {01}", "delay 200",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x001 {${device.zigbeeId}} {}", "delay 500"
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
@@ -299,7 +297,7 @@ def enrollResponse() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
[
|
||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
|
||||
@@ -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 {
|
||||
@@ -301,20 +301,18 @@ def configure() {
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 300 3600 {01}",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200"
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
@@ -322,12 +320,12 @@ def enrollResponse() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
[
|
||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
|
||||
@@ -292,18 +292,16 @@ def configure() {
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 300 3600 {01}",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200"
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
@@ -313,7 +311,7 @@ def enrollResponse() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
[
|
||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
|
||||
@@ -54,33 +54,31 @@
|
||||
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 {
|
||||
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
}
|
||||
/*
|
||||
section {
|
||||
input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false)
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4){
|
||||
tileAttribute ("device.contact", key: "PRIMARY_CONTROL") {
|
||||
multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){
|
||||
tileAttribute ("device.status", key: "PRIMARY_CONTROL") {
|
||||
attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e"
|
||||
attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821"
|
||||
attributeState "garage-open", label:'Open', icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e"
|
||||
attributeState "garage-closed", label:'Closed', icon:"st.doors.garage.garage-closed", backgroundColor:"#79b821"
|
||||
}
|
||||
}
|
||||
standardTile("status", "device.contact", width: 2, height: 2) {
|
||||
standardTile("contact", "device.contact", width: 2, height: 2) {
|
||||
state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
|
||||
state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821")
|
||||
}
|
||||
@@ -112,8 +110,8 @@
|
||||
}
|
||||
|
||||
|
||||
main(["contact", "acceleration", "temperature"])
|
||||
details(["contact", "acceleration", "temperature", "3axis", "battery", "refresh"])
|
||||
main(["status", "acceleration", "temperature"])
|
||||
details(["status", "acceleration", "temperature", "3axis", "battery", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,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,
|
||||
@@ -397,35 +395,34 @@ def getTemperature(value) {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting"
|
||||
|
||||
def configCmds = [
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
def configCmds = [
|
||||
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0xFC02 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0010 0x18 300 3600 {01}",
|
||||
"zcl global send-me-a-report 0xFC02 0x0010 0x18 10 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0012 0x29 300 3600 {01}",
|
||||
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0012 0x29 1 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0013 0x29 300 3600 {01}",
|
||||
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0013 0x29 1 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0014 0x29 300 3600 {01}",
|
||||
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||
|
||||
]
|
||||
@@ -442,7 +439,7 @@ def enrollResponse() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
[
|
||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
|
||||
@@ -297,29 +297,27 @@ def getTemperature(value) {
|
||||
return refreshCmds + enrollResponse()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
def configure() {
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC02 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0xFC02 2 0x18 300 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0xFC02 2 0x18 30 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
@@ -327,7 +325,7 @@ def enrollResponse() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
[
|
||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
|
||||
@@ -275,22 +275,16 @@ def configure() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
|
||||
//"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||
//"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500"
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
@@ -300,7 +294,7 @@ def enrollResponse() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
[
|
||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
|
||||
@@ -253,22 +253,19 @@ def configure() {
|
||||
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
def configCmds = [
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
|
||||
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {0100}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1000",
|
||||
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
"zcl global send-me-a-report 0xFC45 0 0x29 300 3600 {6400}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC45 {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}"
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC45 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0xFC45 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
private hex(value) {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* 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"
|
||||
}
|
||||
|
||||
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,77 @@
|
||||
* 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"
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
161
devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy
Normal file
161
devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 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 YRL220 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}", 3600, 3600, "{01}")
|
||||
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
|
||||
}
|
||||
|
||||
def unlock() {
|
||||
def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_UNLOCK_DOOR}", "{}")
|
||||
log.info "unlock() -- cmds: $cmds"
|
||||
return cmds
|
||||
}
|
||||
|
||||
// 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"
|
||||
// BatteryPercentageRemaining is specified in .5% increments
|
||||
resultMap.value = Integer.parseInt(descMap.value, 16) / 2
|
||||
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,90 @@
|
||||
/**
|
||||
* 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"
|
||||
}
|
||||
|
||||
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,130 @@
|
||||
/**
|
||||
* 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"
|
||||
}
|
||||
|
||||
// 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"
|
||||
|
||||
@@ -11,8 +11,13 @@
|
||||
* 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.
|
||||
*
|
||||
*
|
||||
* SmartThings data is sent from this SmartApp to Initial State. This is event data only for
|
||||
* devices for which the user has authorized. Likewise, Initial State's services call this
|
||||
* SmartApp on the user's behalf to configure Initial State specific parameters. The ToS and
|
||||
* Privacy Policy for Initial State can be found here: https://www.initialstate.com/terms
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Initial State Event Streamer",
|
||||
namespace: "initialstate.events",
|
||||
@@ -28,32 +33,31 @@ import groovy.json.JsonSlurper
|
||||
|
||||
preferences {
|
||||
section("Choose which devices to monitor...") {
|
||||
//input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false
|
||||
input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false
|
||||
input "alarms", "capability.alarm", title: "Alarms", multiple: true, required: false
|
||||
//input "batteries", "capability.battery", title: "Batteries", multiple: true, required: false
|
||||
//input "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false
|
||||
//input "buttons", "capability.button", title: "Buttons", multiple: true, required: false
|
||||
//input "cos", "capability.carbonMonoxideDetector", title: "Carbon Monoxide Detectors", multiple: true, required: false
|
||||
//input "colors", "capability.colorControl", title: "Color Controllers", multiple: true, required: false
|
||||
input "batteries", "capability.battery", title: "Batteries", multiple: true, required: false
|
||||
input "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false
|
||||
input "cos", "capability.carbonMonoxideDetector", title: "Carbon Monoxide Detectors", multiple: true, required: false
|
||||
input "colors", "capability.colorControl", title: "Color Controllers", multiple: true, required: false
|
||||
input "contacts", "capability.contactSensor", title: "Contact Sensors", multiple: true, required: false
|
||||
//input "doorsControllers", "capability.doorControl", title: "Door Controllers", multiple: true, required: false
|
||||
//input "energyMeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false
|
||||
//input "illuminances", "capability.illuminanceMeasurement", title: "Illuminance Meters", multiple: true, required: false
|
||||
input "doorsControllers", "capability.doorControl", title: "Door Controllers", multiple: true, required: false
|
||||
input "energyMeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false
|
||||
input "illuminances", "capability.illuminanceMeasurement", title: "Illuminance Meters", multiple: true, required: false
|
||||
input "locks", "capability.lock", title: "Locks", multiple: true, required: false
|
||||
input "motions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false
|
||||
//input "musicPlayers", "capability.musicPlayer", title: "Music Players", multiple: true, required: false
|
||||
//input "powerMeters", "capability.powerMeter", title: "Power Meters", multiple: true, required: false
|
||||
input "musicPlayers", "capability.musicPlayer", title: "Music Players", multiple: true, required: false
|
||||
input "powerMeters", "capability.powerMeter", title: "Power Meters", multiple: true, required: false
|
||||
input "presences", "capability.presenceSensor", title: "Presence Sensors", multiple: true, required: false
|
||||
input "humidities", "capability.relativeHumidityMeasurement", title: "Humidity Meters", multiple: true, required: false
|
||||
//input "relaySwitches", "capability.relaySwitch", title: "Relay Switches", multiple: true, required: false
|
||||
//input "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", multiple: true, required: false
|
||||
//input "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", multiple: true, required: false
|
||||
//input "peds", "capability.stepSensor", title: "Pedometers", multiple: true, required: false
|
||||
input "relaySwitches", "capability.relaySwitch", title: "Relay Switches", multiple: true, required: false
|
||||
input "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", multiple: true, required: false
|
||||
input "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", multiple: true, required: false
|
||||
input "peds", "capability.stepSensor", title: "Pedometers", multiple: true, required: false
|
||||
input "switches", "capability.switch", title: "Switches", multiple: true, required: false
|
||||
input "switchLevels", "capability.switchLevel", title: "Switch Levels", multiple: true, required: false
|
||||
input "temperatures", "capability.temperatureMeasurement", title: "Temperature Sensors", multiple: true, required: false
|
||||
input "thermostats", "capability.thermostat", title: "Thermostats", multiple: true, required: false
|
||||
//input "valves", "capability.valve", title: "Valves", multiple: true, required: false
|
||||
input "valves", "capability.valve", title: "Valves", multiple: true, required: false
|
||||
input "waterSensors", "capability.waterSensor", title: "Water Sensors", multiple: true, required: false
|
||||
}
|
||||
}
|
||||
@@ -74,77 +78,71 @@ mappings {
|
||||
}
|
||||
|
||||
def subscribeToEvents() {
|
||||
/*if (accelerometers != null) {
|
||||
if (accelerometers != null) {
|
||||
subscribe(accelerometers, "acceleration", genericHandler)
|
||||
}*/
|
||||
}
|
||||
if (alarms != null) {
|
||||
subscribe(alarms, "alarm", genericHandler)
|
||||
}
|
||||
/*if (batteries != null) {
|
||||
if (batteries != null) {
|
||||
subscribe(batteries, "battery", genericHandler)
|
||||
}*/
|
||||
/*if (beacons != null) {
|
||||
}
|
||||
if (beacons != null) {
|
||||
subscribe(beacons, "presence", genericHandler)
|
||||
}*/
|
||||
/*
|
||||
if (buttons != null) {
|
||||
subscribe(buttons, "button", genericHandler)
|
||||
}*/
|
||||
/*if (cos != null) {
|
||||
}
|
||||
|
||||
if (cos != null) {
|
||||
subscribe(cos, "carbonMonoxide", genericHandler)
|
||||
}*/
|
||||
/*if (colors != null) {
|
||||
}
|
||||
if (colors != null) {
|
||||
subscribe(colors, "hue", genericHandler)
|
||||
subscribe(colors, "saturation", genericHandler)
|
||||
subscribe(colors, "color", genericHandler)
|
||||
}*/
|
||||
}
|
||||
if (contacts != null) {
|
||||
subscribe(contacts, "contact", genericHandler)
|
||||
}
|
||||
/*if (doorsControllers != null) {
|
||||
subscribe(doorsControllers, "door", genericHandler)
|
||||
}*/
|
||||
/*if (energyMeters != null) {
|
||||
if (energyMeters != null) {
|
||||
subscribe(energyMeters, "energy", genericHandler)
|
||||
}*/
|
||||
/*if (illuminances != null) {
|
||||
}
|
||||
if (illuminances != null) {
|
||||
subscribe(illuminances, "illuminance", genericHandler)
|
||||
}*/
|
||||
}
|
||||
if (locks != null) {
|
||||
subscribe(locks, "lock", genericHandler)
|
||||
}
|
||||
if (motions != null) {
|
||||
subscribe(motions, "motion", genericHandler)
|
||||
}
|
||||
/*if (musicPlayers != null) {
|
||||
if (musicPlayers != null) {
|
||||
subscribe(musicPlayers, "status", genericHandler)
|
||||
subscribe(musicPlayers, "level", genericHandler)
|
||||
subscribe(musicPlayers, "trackDescription", genericHandler)
|
||||
subscribe(musicPlayers, "trackData", genericHandler)
|
||||
subscribe(musicPlayers, "mute", genericHandler)
|
||||
}*/
|
||||
/*if (powerMeters != null) {
|
||||
}
|
||||
if (powerMeters != null) {
|
||||
subscribe(powerMeters, "power", genericHandler)
|
||||
}*/
|
||||
}
|
||||
if (presences != null) {
|
||||
subscribe(presences, "presence", genericHandler)
|
||||
}
|
||||
if (humidities != null) {
|
||||
subscribe(humidities, "humidity", genericHandler)
|
||||
}
|
||||
/*if (relaySwitches != null) {
|
||||
if (relaySwitches != null) {
|
||||
subscribe(relaySwitches, "switch", genericHandler)
|
||||
}*/
|
||||
/*if (sleepSensors != null) {
|
||||
}
|
||||
if (sleepSensors != null) {
|
||||
subscribe(sleepSensors, "sleeping", genericHandler)
|
||||
}*/
|
||||
/*if (smokeDetectors != null) {
|
||||
}
|
||||
if (smokeDetectors != null) {
|
||||
subscribe(smokeDetectors, "smoke", genericHandler)
|
||||
}*/
|
||||
/*if (peds != null) {
|
||||
}
|
||||
if (peds != null) {
|
||||
subscribe(peds, "steps", genericHandler)
|
||||
subscribe(peds, "goal", genericHandler)
|
||||
}*/
|
||||
}
|
||||
if (switches != null) {
|
||||
subscribe(switches, "switch", genericHandler)
|
||||
}
|
||||
@@ -163,9 +161,9 @@ def subscribeToEvents() {
|
||||
subscribe(thermostats, "thermostatFanMode", genericHandler)
|
||||
subscribe(thermostats, "thermostatOperatingState", genericHandler)
|
||||
}
|
||||
/*if (valves != null) {
|
||||
if (valves != null) {
|
||||
subscribe(valves, "contact", genericHandler)
|
||||
}*/
|
||||
}
|
||||
if (waterSensors != null) {
|
||||
subscribe(waterSensors, "water", genericHandler)
|
||||
}
|
||||
@@ -173,23 +171,23 @@ def subscribeToEvents() {
|
||||
|
||||
def getAccessKey() {
|
||||
log.trace "get access key"
|
||||
if (state.accessKey == null) {
|
||||
if (atomicState.accessKey == null) {
|
||||
httpError(404, "Access Key Not Found")
|
||||
} else {
|
||||
[
|
||||
accessKey: state.accessKey
|
||||
accessKey: atomicState.accessKey
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def getBucketKey() {
|
||||
log.trace "get bucket key"
|
||||
if (state.bucketKey == null) {
|
||||
if (atomicState.bucketKey == null) {
|
||||
httpError(404, "Bucket key Not Found")
|
||||
} else {
|
||||
[
|
||||
bucketKey: state.bucketKey,
|
||||
bucketName: state.bucketName
|
||||
bucketKey: atomicState.bucketKey,
|
||||
bucketName: atomicState.bucketName
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -202,53 +200,94 @@ def setBucketKey() {
|
||||
log.debug "bucket name: $newBucketName"
|
||||
log.debug "bucket key: $newBucketKey"
|
||||
|
||||
if (newBucketKey && (newBucketKey != state.bucketKey || newBucketName != state.bucketName)) {
|
||||
state.bucketKey = "$newBucketKey"
|
||||
state.bucketName = "$newBucketName"
|
||||
state.isBucketCreated = false
|
||||
if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) {
|
||||
atomicState.bucketKey = "$newBucketKey"
|
||||
atomicState.bucketName = "$newBucketName"
|
||||
atomicState.isBucketCreated = false
|
||||
}
|
||||
|
||||
tryCreateBucket()
|
||||
}
|
||||
|
||||
def setAccessKey() {
|
||||
log.trace "set access key"
|
||||
def newAccessKey = request.JSON?.accessKey
|
||||
def newGrokerSubdomain = request.JSON?.grokerSubdomain
|
||||
|
||||
if (newAccessKey && newAccessKey != state.accessKey) {
|
||||
state.accessKey = "$newAccessKey"
|
||||
state.isBucketCreated = false
|
||||
if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) {
|
||||
atomicState.grokerSubdomain = "$newGrokerSubdomain"
|
||||
atomicState.isBucketCreated = false
|
||||
}
|
||||
|
||||
if (newAccessKey && newAccessKey != atomicState.accessKey) {
|
||||
atomicState.accessKey = "$newAccessKey"
|
||||
atomicState.isBucketCreated = false
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
|
||||
atomicState.version = "1.0.18"
|
||||
subscribeToEvents()
|
||||
|
||||
state.isBucketCreated = false
|
||||
atomicState.isBucketCreated = false
|
||||
atomicState.grokerSubdomain = "groker"
|
||||
atomicState.eventBuffer = []
|
||||
|
||||
runEvery15Minutes(flushBuffer)
|
||||
|
||||
log.debug "installed (version $atomicState.version)"
|
||||
}
|
||||
|
||||
def updated() {
|
||||
atomicState.version = "1.0.18"
|
||||
unsubscribe()
|
||||
|
||||
if (state.bucketKey != null && state.accessKey != null) {
|
||||
state.isBucketCreated = false
|
||||
if (atomicState.bucketKey != null && atomicState.accessKey != null) {
|
||||
atomicState.isBucketCreated = false
|
||||
}
|
||||
|
||||
if (atomicState.eventBuffer == null) {
|
||||
atomicState.eventBuffer = []
|
||||
}
|
||||
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
|
||||
atomicState.grokerSubdomain = "groker"
|
||||
}
|
||||
|
||||
subscribeToEvents()
|
||||
|
||||
log.debug "updated (version $atomicState.version)"
|
||||
}
|
||||
|
||||
def createBucket() {
|
||||
def uninstalled() {
|
||||
log.debug "uninstalled (version $atomicState.version)"
|
||||
}
|
||||
|
||||
if (!state.bucketName) {
|
||||
state.bucketName = state.bucketKey
|
||||
def tryCreateBucket() {
|
||||
|
||||
// can't ship events if there is no grokerSubdomain
|
||||
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
|
||||
log.error "streaming url is currently null"
|
||||
return
|
||||
}
|
||||
|
||||
// if the bucket has already been created, no need to continue
|
||||
if (atomicState.isBucketCreated) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!atomicState.bucketName) {
|
||||
atomicState.bucketName = atomicState.bucketKey
|
||||
}
|
||||
def bucketName = "${state.bucketName}"
|
||||
def bucketKey = "${state.bucketKey}"
|
||||
def accessKey = "${state.accessKey}"
|
||||
if (!atomicState.accessKey) {
|
||||
return
|
||||
}
|
||||
def bucketName = "${atomicState.bucketName}"
|
||||
def bucketKey = "${atomicState.bucketKey}"
|
||||
def accessKey = "${atomicState.accessKey}"
|
||||
|
||||
def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}")
|
||||
|
||||
def bucketCreatePost = [
|
||||
uri: 'https://groker.initialstate.com/api/buckets',
|
||||
uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/buckets",
|
||||
headers: [
|
||||
"Content-Type": "application/json",
|
||||
"X-IS-AccessKey": accessKey
|
||||
@@ -258,10 +297,20 @@ def createBucket() {
|
||||
|
||||
log.debug bucketCreatePost
|
||||
|
||||
httpPostJson(bucketCreatePost) {
|
||||
log.debug "bucket posted"
|
||||
state.isBucketCreated = true
|
||||
try {
|
||||
// Create a bucket on Initial State so the data has a logical grouping
|
||||
httpPostJson(bucketCreatePost) { resp ->
|
||||
log.debug "bucket posted"
|
||||
if (resp.status >= 400) {
|
||||
log.error "bucket not created successfully"
|
||||
} else {
|
||||
atomicState.isBucketCreated = true
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log.error "bucket creation error: $e"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def genericHandler(evt) {
|
||||
@@ -273,33 +322,80 @@ def genericHandler(evt) {
|
||||
}
|
||||
def value = "$evt.value"
|
||||
|
||||
tryCreateBucket()
|
||||
|
||||
eventHandler(key, value)
|
||||
}
|
||||
|
||||
def eventHandler(name, value) {
|
||||
// This is a handler function for flushing the event buffer
|
||||
// after a specified amount of time to reduce the load on ST servers
|
||||
def flushBuffer() {
|
||||
log.trace "About to flush the buffer on schedule"
|
||||
if (atomicState.eventBuffer != null && atomicState.eventBuffer.size() > 0) {
|
||||
tryShipEvents()
|
||||
}
|
||||
}
|
||||
|
||||
if (state.accessKey == null || state.bucketKey == null) {
|
||||
def eventHandler(name, value) {
|
||||
log.debug atomicState.eventBuffer
|
||||
|
||||
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
|
||||
|
||||
atomicState.eventBuffer = eventBuffer
|
||||
|
||||
if (eventBuffer.size() >= 10) {
|
||||
tryShipEvents()
|
||||
}
|
||||
}
|
||||
|
||||
// a helper function for shipping the atomicState.eventBuffer to Initial State
|
||||
def tryShipEvents() {
|
||||
|
||||
// can't ship events if there is no grokerSubdomain
|
||||
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
|
||||
log.error "streaming url is currently null"
|
||||
return
|
||||
}
|
||||
// can't ship if access key and bucket key are null, so finish trying
|
||||
if (atomicState.accessKey == null || atomicState.bucketKey == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!state.isBucketCreated) {
|
||||
createBucket()
|
||||
}
|
||||
|
||||
def eventBody = new JsonSlurper().parseText("[{\"key\": \"$name\", \"value\": \"$value\"}]")
|
||||
def eventPost = [
|
||||
uri: 'https://groker.initialstate.com/api/events',
|
||||
uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/events",
|
||||
headers: [
|
||||
"Content-Type": "application/json",
|
||||
"X-IS-BucketKey": "${state.bucketKey}",
|
||||
"X-IS-AccessKey": "${state.accessKey}"
|
||||
"X-IS-BucketKey": "${atomicState.bucketKey}",
|
||||
"X-IS-AccessKey": "${atomicState.accessKey}",
|
||||
"Accept-Version": "0.0.2"
|
||||
],
|
||||
body: eventBody
|
||||
body: atomicState.eventBuffer
|
||||
]
|
||||
|
||||
log.debug eventPost
|
||||
|
||||
httpPostJson(eventPost) {
|
||||
log.debug "event data posted"
|
||||
try {
|
||||
// post the events to initial state
|
||||
httpPostJson(eventPost) { resp ->
|
||||
log.debug "shipped events and got ${resp.status}"
|
||||
if (resp.status >= 400) {
|
||||
log.error "shipping failed... ${resp.data}"
|
||||
} else {
|
||||
// clear the buffer
|
||||
atomicState.eventBuffer = []
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log.error "shipping events failed: $e"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
definition(
|
||||
name: "Hue (Connect)",
|
||||
namespace: "smartthings",
|
||||
@@ -64,10 +64,14 @@ def bridgeDiscovery(params=[:])
|
||||
def options = bridges ?: []
|
||||
def numFound = options.size() ?: 0
|
||||
|
||||
if(!state.subscribe) {
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
state.subscribe = true
|
||||
}
|
||||
if (numFound == 0 && state.bridgeRefreshCount > 25) {
|
||||
log.trace "Cleaning old bridges memory"
|
||||
state.bridges = [:]
|
||||
state.bridgeRefreshCount = 0
|
||||
app.updateSetting("selectedHue", "")
|
||||
}
|
||||
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
|
||||
//bridge discovery request every 15 //25 seconds
|
||||
if((bridgeRefreshCount % 5) == 0) {
|
||||
@@ -94,11 +98,20 @@ def bridgeLinking()
|
||||
|
||||
def nextPage = ""
|
||||
def title = "Linking with your Hue"
|
||||
def paragraphText = "Press the button on your Hue Bridge to setup a link."
|
||||
def paragraphText
|
||||
def hueimage = null
|
||||
if (selectedHue) {
|
||||
paragraphText = "Press the button on your Hue Bridge to setup a link. "
|
||||
hueimage = "http://huedisco.mediavibe.nl/wp-content/uploads/2013/09/pair-bridge.png"
|
||||
} else {
|
||||
paragraphText = "You haven't selected a Hue Bridge, please Press \"Done\" and select one before clicking next."
|
||||
hueimage = null
|
||||
}
|
||||
if (state.username) { //if discovery worked
|
||||
nextPage = "bulbDiscovery"
|
||||
title = "Success! - click 'Next'"
|
||||
title = "Success!"
|
||||
paragraphText = "Linking to your hub was a success! Please click 'Next'!"
|
||||
hueimage = null
|
||||
}
|
||||
|
||||
if((linkRefreshcount % 2) == 0 && !state.username) {
|
||||
@@ -106,30 +119,39 @@ def bridgeLinking()
|
||||
}
|
||||
|
||||
return dynamicPage(name:"bridgeBtnPush", title:title, nextPage:nextPage, refreshInterval:refreshInterval) {
|
||||
section("Button Press") {
|
||||
section("") {
|
||||
paragraph """${paragraphText}"""
|
||||
if (hueimage != null)
|
||||
image "${hueimage}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def bulbDiscovery()
|
||||
{
|
||||
def bulbDiscovery() {
|
||||
int bulbRefreshCount = !state.bulbRefreshCount ? 0 : state.bulbRefreshCount as int
|
||||
state.bulbRefreshCount = bulbRefreshCount + 1
|
||||
def refreshInterval = 3
|
||||
|
||||
def options = bulbsDiscovered() ?: []
|
||||
def numFound = options.size() ?: 0
|
||||
|
||||
state.inBulbDiscovery = true
|
||||
def bridge = null
|
||||
if (selectedHue) {
|
||||
bridge = getChildDevice(selectedHue)
|
||||
subscribe(bridge, "bulbList", bulbListData)
|
||||
}
|
||||
state.bridgeRefreshCount = 0
|
||||
def bulboptions = bulbsDiscovered() ?: [:]
|
||||
def numFound = bulboptions.size() ?: 0
|
||||
if (numFound == 0)
|
||||
app.updateSetting("selectedBulbs", "")
|
||||
|
||||
if((bulbRefreshCount % 3) == 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 {
|
||||
section {
|
||||
def title = getBridgeIP() ? "Hue bridge (${getBridgeIP()})" : "Find bridges"
|
||||
href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true]
|
||||
|
||||
@@ -194,24 +216,29 @@ Map bridgesDiscovered() {
|
||||
|
||||
Map bulbsDiscovered() {
|
||||
def bulbs = getHueBulbs()
|
||||
def map = [:]
|
||||
def bulbmap = [:]
|
||||
if (bulbs instanceof java.util.Map) {
|
||||
bulbs.each {
|
||||
def value = "${it?.value?.name}"
|
||||
def key = app.id +"/"+ it?.value?.id
|
||||
map["${key}"] = value
|
||||
def value = "${it.value.name}"
|
||||
def key = app.id +"/"+ it.value.id
|
||||
bulbmap["${key}"] = value
|
||||
}
|
||||
} else { //backwards compatable
|
||||
bulbs.each {
|
||||
def value = "${it?.name}"
|
||||
def key = app.id +"/"+ it?.id
|
||||
map["${key}"] = value
|
||||
def value = "${it.name}"
|
||||
def key = app.id +"/"+ it.id
|
||||
logg += "$value - $key, "
|
||||
bulbmap["${key}"] = value
|
||||
}
|
||||
}
|
||||
map
|
||||
return bulbmap
|
||||
}
|
||||
|
||||
def getHueBulbs() {
|
||||
def bulbListData(evt) {
|
||||
state.bulbs = evt.jsonData
|
||||
}
|
||||
|
||||
Map getHueBulbs() {
|
||||
state.bulbs = state.bulbs ?: [:]
|
||||
}
|
||||
|
||||
@@ -231,24 +258,19 @@ def installed() {
|
||||
def updated() {
|
||||
log.trace "Updated with settings: ${settings}"
|
||||
unschedule()
|
||||
unsubscribe()
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
log.debug "Initializing"
|
||||
state.subscribe = false
|
||||
state.bridgeSelectedOverride = false
|
||||
def bridge = null
|
||||
|
||||
log.debug "Initializing"
|
||||
unsubscribe(bridge)
|
||||
state.inBulbDiscovery = false
|
||||
state.bridgeRefreshCount = 0
|
||||
state.bulbRefreshCount = 0
|
||||
if (selectedHue) {
|
||||
addBridge()
|
||||
bridge = getChildDevice(selectedHue)
|
||||
subscribe(bridge, "bulbList", bulbListHandler)
|
||||
}
|
||||
|
||||
if (selectedBulbs) {
|
||||
addBulbs()
|
||||
addBridge()
|
||||
addBulbs()
|
||||
doDeviceSync()
|
||||
runEvery5Minutes("doDeviceSync")
|
||||
}
|
||||
@@ -263,27 +285,34 @@ def manualRefresh() {
|
||||
|
||||
def uninstalled(){
|
||||
state.bridges = [:]
|
||||
state.subscribe = false
|
||||
state.username = null
|
||||
}
|
||||
|
||||
// Handles events to add new bulbs
|
||||
def bulbListHandler(evt) {
|
||||
def bulbs = [:]
|
||||
log.trace "Adding bulbs to state..."
|
||||
state.bridgeProcessedLightList = true
|
||||
evt.jsonData.each { k,v ->
|
||||
log.trace "$k: $v"
|
||||
if (v instanceof Map) {
|
||||
bulbs[k] = [id: k, name: v.name, type: v.type, hub:evt.value]
|
||||
}
|
||||
}
|
||||
state.bulbs = bulbs
|
||||
log.info "${bulbs.size()} bulbs found"
|
||||
def bulbListHandler(hub, data = "") {
|
||||
def msg = "Bulbs list not processed. Only while in settings menu."
|
||||
def bulbs = [:]
|
||||
if (state.inBulbDiscovery) {
|
||||
def logg = ""
|
||||
log.trace "Adding bulbs to state..."
|
||||
state.bridgeProcessedLightList = true
|
||||
def object = new groovy.json.JsonSlurper().parseText(data)
|
||||
object.each { k,v ->
|
||||
if (v instanceof Map)
|
||||
bulbs[k] = [id: k, name: v.name, type: v.type, hub:hub]
|
||||
}
|
||||
}
|
||||
def bridge = null
|
||||
if (selectedHue)
|
||||
bridge = getChildDevice(selectedHue)
|
||||
bridge.sendEvent(name: "bulbList", value: hub, data: bulbs, isStateChange: true, displayed: false)
|
||||
msg = "${bulbs.size()} bulbs found. ${bulbs}"
|
||||
return msg
|
||||
}
|
||||
|
||||
def addBulbs() {
|
||||
def bulbs = getHueBulbs()
|
||||
selectedBulbs.each { dni ->
|
||||
selectedBulbs?.each { dni ->
|
||||
def d = getChildDevice(dni)
|
||||
if(!d) {
|
||||
def newHueBulb
|
||||
@@ -294,7 +323,7 @@ def addBulbs() {
|
||||
} else {
|
||||
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
||||
}
|
||||
} else {
|
||||
} else {
|
||||
//backwards compatable
|
||||
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
|
||||
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name])
|
||||
@@ -322,7 +351,7 @@ def addBridge() {
|
||||
def d = getChildDevice(selectedHue)
|
||||
if(!d) {
|
||||
// compatibility with old devices
|
||||
def newbridge = true
|
||||
def newbridge = true
|
||||
childDevices.each {
|
||||
if (it.getDeviceDataByName("mac")) {
|
||||
def newDNI = "${it.getDeviceDataByName("mac")}"
|
||||
@@ -332,22 +361,27 @@ def addBridge() {
|
||||
it.setDeviceNetworkId("${newDNI}")
|
||||
if (oldDNI == selectedHue)
|
||||
app.updateSetting("selectedHue", newDNI)
|
||||
newbridge = false
|
||||
newbridge = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newbridge) {
|
||||
d = addChildDevice("smartthings", "Hue Bridge", selectedHue, vbridge.value.hub)
|
||||
log.debug "created ${d.displayName} with id ${d.deviceNetworkId}"
|
||||
def childDevice = getChildDevice(d.deviceNetworkId)
|
||||
childDevice.sendEvent(name: "serialNumber", value: vbridge.value.serialNumber)
|
||||
if (vbridge.value.ip && vbridge.value.port) {
|
||||
if (vbridge.value.ip.contains("."))
|
||||
if (vbridge.value.ip.contains(".")) {
|
||||
childDevice.sendEvent(name: "networkAddress", value: vbridge.value.ip + ":" + vbridge.value.port)
|
||||
else
|
||||
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
|
||||
} else
|
||||
childDevice.updateDataValue("networkAddress", vbridge.value.ip + ":" + vbridge.value.port)
|
||||
} else {
|
||||
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
|
||||
childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
|
||||
}
|
||||
} else {
|
||||
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
|
||||
childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $selectedHue already exists"
|
||||
@@ -355,7 +389,6 @@ def addBridge() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def locationHandler(evt) {
|
||||
def description = evt.description
|
||||
log.trace "Location: $description"
|
||||
@@ -397,8 +430,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..."
|
||||
@@ -406,7 +442,8 @@ def locationHandler(evt) {
|
||||
dstate.port = port
|
||||
dstate.name = "Philips hue ($ip)"
|
||||
d.sendEvent(name:"networkAddress", value: host)
|
||||
}
|
||||
d.updateDataValue("networkAddress", host)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -454,17 +491,13 @@ def locationHandler(evt) {
|
||||
|
||||
def doDeviceSync(){
|
||||
log.trace "Doing Hue Device Sync!"
|
||||
|
||||
//shrink the large bulb lists
|
||||
convertBulbListToMap()
|
||||
|
||||
poll()
|
||||
|
||||
if(!state.subscribe) {
|
||||
try {
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
state.subscribe = true
|
||||
}
|
||||
|
||||
} catch (all) {
|
||||
log.trace "Subscription already exist"
|
||||
}
|
||||
discoverBridges()
|
||||
}
|
||||
|
||||
@@ -473,44 +506,49 @@ def doDeviceSync(){
|
||||
/////////////////////////////////////
|
||||
|
||||
def parse(childDevice, description) {
|
||||
def parsedEvent = parseLanMessage(description)
|
||||
def parsedEvent = parseLanMessage(description)
|
||||
if (parsedEvent.headers && parsedEvent.body) {
|
||||
def headerString = parsedEvent.headers.toString()
|
||||
if (headerString?.contains("json")) {
|
||||
def body = new groovy.json.JsonSlurper().parseText(parsedEvent.body)
|
||||
if (body instanceof java.util.HashMap)
|
||||
{ //poll response
|
||||
def bodyString = parsedEvent.body.toString()
|
||||
if (headerString?.contains("json")) {
|
||||
def body
|
||||
try {
|
||||
body = new groovy.json.JsonSlurper().parseText(bodyString)
|
||||
} catch (all) {
|
||||
log.warn "Parsing Body failed - trying again..."
|
||||
poll()
|
||||
}
|
||||
if (body instanceof java.util.HashMap) {
|
||||
//poll response
|
||||
def bulbs = getChildDevices()
|
||||
//for each bulb
|
||||
for (bulb in body) {
|
||||
def d = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"}
|
||||
def d = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"}
|
||||
if (d) {
|
||||
if (bulb.value.state?.reachable) {
|
||||
sendEvent(d.deviceNetworkId, [name: "switch", value: bulb.value?.state?.on ? "on" : "off"])
|
||||
sendEvent(d.deviceNetworkId, [name: "level", value: Math.round(bulb.value.state.bri * 100 / 255)])
|
||||
if (bulb.value.state.sat) {
|
||||
def hue = Math.min(Math.round(bulb.value.state.hue * 100 / 65535), 65535) as int
|
||||
def sat = Math.round(bulb.value.state.sat * 100 / 255) as int
|
||||
def hex = colorUtil.hslToHex(hue, sat)
|
||||
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
|
||||
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
|
||||
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
|
||||
}
|
||||
} else {
|
||||
sendEvent(d.deviceNetworkId, [name: "switch", value: "off"])
|
||||
sendEvent(d.deviceNetworkId, [name: "level", value: 100])
|
||||
if (bulb.value.state.sat) {
|
||||
def hue = 23
|
||||
def sat = 56
|
||||
def hex = colorUtil.hslToHex(23, 56)
|
||||
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
|
||||
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
|
||||
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
|
||||
}
|
||||
}
|
||||
sendEvent(d.deviceNetworkId, [name: "switch", value: bulb.value?.state?.on ? "on" : "off"])
|
||||
sendEvent(d.deviceNetworkId, [name: "level", value: Math.round(bulb.value.state.bri * 100 / 255)])
|
||||
if (bulb.value.state.sat) {
|
||||
def hue = Math.min(Math.round(bulb.value.state.hue * 100 / 65535), 65535) as int
|
||||
def sat = Math.round(bulb.value.state.sat * 100 / 255) as int
|
||||
def hex = colorUtil.hslToHex(hue, sat)
|
||||
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
|
||||
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
|
||||
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
|
||||
}
|
||||
} else {
|
||||
sendEvent(d.deviceNetworkId, [name: "switch", value: "off"])
|
||||
sendEvent(d.deviceNetworkId, [name: "level", value: 100])
|
||||
if (bulb.value.state.sat) {
|
||||
def hue = 23
|
||||
def sat = 56
|
||||
def hex = colorUtil.hslToHex(23, 56)
|
||||
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
|
||||
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
|
||||
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{ //put response
|
||||
@@ -559,25 +597,25 @@ def parse(childDevice, description) {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug "parse - got something other than headers,body..."
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
def on(childDevice, transition = 4) {
|
||||
def on(childDevice, transition_deprecated = 0) {
|
||||
log.debug "Executing 'on'"
|
||||
// Assume bulb is off if no current state is found for level to avoid bulbs getting stuck in off after initial discovery
|
||||
def percent = childDevice.device?.currentValue("level") as Integer ?: 0
|
||||
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, transitiontime: transition])
|
||||
put("lights/${getId(childDevice)}/state", [bri: level, on: true])
|
||||
return "level: $percent"
|
||||
}
|
||||
|
||||
def off(childDevice, transition = 4) {
|
||||
def off(childDevice, transition_deprecated = 0) {
|
||||
log.debug "Executing 'off'"
|
||||
put("lights/${getId(childDevice)}/state", [on: false, transitiontime: transition])
|
||||
put("lights/${getId(childDevice)}/state", [on: false])
|
||||
return "level: 0"
|
||||
}
|
||||
|
||||
def setLevel(childDevice, percent) {
|
||||
@@ -598,19 +636,21 @@ def setHue(childDevice, percent) {
|
||||
put("lights/${getId(childDevice)}/state", [hue: level])
|
||||
}
|
||||
|
||||
def setColor(childDevice, color, alert = "none", transition = 4) {
|
||||
log.debug "Executing 'setColor($color)'"
|
||||
def hue = Math.min(Math.round(color.hue * 65535 / 100), 65535)
|
||||
def sat = Math.min(Math.round(color.saturation * 255 / 100), 255)
|
||||
def setColor(childDevice, huesettings, alert_deprecated = "", transition_deprecated = 0) {
|
||||
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)
|
||||
def alert = huesettings.alert ? huesettings.alert : "none"
|
||||
def transition = huesettings.transition ? huesettings.transition : 4
|
||||
|
||||
def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition]
|
||||
if (color.level != null) {
|
||||
value.bri = Math.min(Math.round(color.level * 255 / 100), 255)
|
||||
if (huesettings.level != null) {
|
||||
value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
|
||||
value.on = value.bri > 0
|
||||
}
|
||||
|
||||
if (color.switch) {
|
||||
value.on = color.switch == "on"
|
||||
if (huesettings.switch) {
|
||||
value.on = huesettings.switch == "on"
|
||||
}
|
||||
|
||||
log.debug "sending command $value"
|
||||
@@ -640,15 +680,19 @@ private getId(childDevice) {
|
||||
private poll() {
|
||||
def host = getBridgeIP()
|
||||
def uri = "/api/${state.username}/lights/"
|
||||
log.debug "GET: $host$uri"
|
||||
sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1
|
||||
try {
|
||||
sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1
|
||||
HOST: ${host}
|
||||
|
||||
""", physicalgraph.device.Protocol.LAN, selectedHue))
|
||||
} catch (all) {
|
||||
log.warn "Parsing Body failed - trying again..."
|
||||
doDeviceSync()
|
||||
}
|
||||
}
|
||||
|
||||
private put(path, body) {
|
||||
def host = getBridgeIP()
|
||||
def host = getBridgeIP()
|
||||
def uri = "/api/${state.username}/$path"
|
||||
def bodyJSON = new groovy.json.JsonBuilder(body).toString()
|
||||
def length = bodyJSON.getBytes().size().toString()
|
||||
@@ -668,12 +712,21 @@ ${bodyJSON}
|
||||
private getBridgeIP() {
|
||||
def host = null
|
||||
if (selectedHue) {
|
||||
def d = getChildDevice(dni)
|
||||
if (d)
|
||||
host = d.latestState('networkAddress').stringValue
|
||||
def d = getChildDevice(selectedHue)
|
||||
if (d) {
|
||||
if (d.getDeviceDataByName("networkAddress"))
|
||||
host = d.getDeviceDataByName("networkAddress")
|
||||
else
|
||||
host = d.latestState('networkAddress').stringValue
|
||||
}
|
||||
if (host == null || host == "") {
|
||||
def serialNumber = selectedHue
|
||||
def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value
|
||||
if (!bridge) {
|
||||
//failed because mac address sent from hub is wrong and doesn't match the hue's real mac address and serial number
|
||||
//in this case we will look up the bridge by comparing the incorrect mac addresses
|
||||
bridge = getHueBridges().find { it?.value?.mac?.equalsIgnoreCase(serialNumber) }?.value
|
||||
}
|
||||
if (bridge?.ip && bridge?.port) {
|
||||
if (bridge?.ip.contains("."))
|
||||
host = "${bridge?.ip}:${bridge?.port}"
|
||||
@@ -681,9 +734,9 @@ private getBridgeIP() {
|
||||
host = "${convertHexToIP(bridge?.ip)}:${convertHexToInt(bridge?.port)}"
|
||||
} else if (bridge?.networkAddress && bridge?.deviceAddress)
|
||||
host = "${convertHexToIP(bridge?.networkAddress)}:${convertHexToInt(bridge?.deviceAddress)}"
|
||||
}
|
||||
}
|
||||
log.trace "Bridge: $selectedHue - Host: $host"
|
||||
}
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ def authPage() {
|
||||
|
||||
def options = locationOptions() ?: []
|
||||
def count = options.size()
|
||||
def refreshInterval = 3
|
||||
|
||||
return dynamicPage(name:"Credentials", title:"Select devices...", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
||||
section("Select your location") {
|
||||
@@ -372,9 +373,9 @@ def updateDevices() {
|
||||
data["color"] = colorUtil.hslToHex((device.color.hue / 3.6) as int, (device.color.saturation * 100) as int)
|
||||
data["hue"] = device.color.hue / 3.6
|
||||
data["saturation"] = device.color.saturation * 100
|
||||
childDevice = addChildDevice("lifx", "LIFX Color Bulb", device.id, null, data)
|
||||
childDevice = addChildDevice("smartthings", "LIFX Color Bulb", device.id, null, data)
|
||||
} else {
|
||||
childDevice = addChildDevice("lifx", "LIFX White Bulb", device.id, null, data)
|
||||
childDevice = addChildDevice("smartthings", "LIFX White Bulb", device.id, null, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -390,4 +391,4 @@ def refreshDevices() {
|
||||
getChildDevices().each { device ->
|
||||
device.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@
|
||||
definition(
|
||||
name: "Logitech Harmony (Connect)",
|
||||
namespace: "smartthings",
|
||||
author: "Juan Pablo Risso",
|
||||
author: "SmartThings",
|
||||
description: "Allows you to integrate your Logitech Harmony account with SmartThings.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.png",
|
||||
@@ -394,7 +394,9 @@ def discovery() {
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
log.warn "Connection to the hub timed out. Please restart the hub and try again."
|
||||
state.resethub = true
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn "Hostname in certificate didn't match. Please try again later."
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -459,7 +461,9 @@ def activity(dni,mode) {
|
||||
msg = ex
|
||||
state.aux = 0
|
||||
}
|
||||
}
|
||||
} catch(Exception ex) {
|
||||
msg = ex
|
||||
}
|
||||
runIn(10, "poll", [overwrite: true])
|
||||
return msg
|
||||
}
|
||||
@@ -517,7 +521,9 @@ def poll() {
|
||||
state.remove("HarmonyAccessToken")
|
||||
return "Harmony Access token has expired"
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,7 +556,9 @@ def getActivityList() {
|
||||
log.trace e
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
log.trace e
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
return activity
|
||||
}
|
||||
@@ -565,9 +573,9 @@ def getActivityName(activity,hubId) {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
actname = response.data.data.activities[activity].name
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
return actname
|
||||
}
|
||||
@@ -585,9 +593,9 @@ def getActivityId(activity,hubId) {
|
||||
actid = it.key
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
return actid
|
||||
}
|
||||
@@ -602,9 +610,9 @@ def getHubName(hubId) {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
hubname = response.data.data.name
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
return hubname
|
||||
}
|
||||
|
||||
@@ -79,12 +79,9 @@ def scenePage(params=[:]) {
|
||||
href "devicePage", title: "Show Device States", params: [sceneId:sceneId], description: "", state: sceneIsDefined(sceneId) ? "complete" : "incomplete"
|
||||
}
|
||||
|
||||
if (sceneId == currentSceneId) {
|
||||
section {
|
||||
href "saveStatesPage", title: "Record Current Device States", params: [sceneId:sceneId], description: ""
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
href "saveStatesPage", title: "Record Current Device States", params: [sceneId:sceneId], description: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +222,7 @@ private restoreStates(sceneId) {
|
||||
if (type == "level") {
|
||||
log.debug "${light.displayName} level is '$level'"
|
||||
if (level != null) {
|
||||
light.setLevel(value)
|
||||
light.setLevel(level)
|
||||
}
|
||||
}
|
||||
else if (type == "color") {
|
||||
|
||||
@@ -346,18 +346,20 @@ private getSensorJSON(id, key) {
|
||||
|
||||
def sensorUrl = "${wattvisionBaseURL()}/partners/smartthings/sensor_list?api_id=${id}&api_key=${key}"
|
||||
|
||||
httpGet(uri: sensorUrl) { response ->
|
||||
httpGet(uri: sensorUrl) { response ->
|
||||
|
||||
def json = new org.json.JSONObject(response.data)
|
||||
def sensors = [:]
|
||||
|
||||
state.sensors = json
|
||||
|
||||
json.each { sensorId, sensorName ->
|
||||
response.data.each { sensorId, sensorName ->
|
||||
sensors[sensorId] = sensorName
|
||||
createChild(sensorId, sensorName)
|
||||
}
|
||||
}
|
||||
|
||||
state.sensors = sensors
|
||||
|
||||
return "success"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def createChild(sensorId, sensorName) {
|
||||
|
||||
@@ -0,0 +1,775 @@
|
||||
/**
|
||||
* Title: Withings Service Manager
|
||||
* Description: Connect Your Withings Devices
|
||||
*
|
||||
* Author: steve
|
||||
* Date: 1/9/15
|
||||
*
|
||||
*
|
||||
* Copyright 2015 steve
|
||||
*
|
||||
* 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: "Withings Manager",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Connect With Withings",
|
||||
category: "",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/withings.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/withings%402x.png",
|
||||
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Partner/withings%402x.png",
|
||||
oauth: true
|
||||
) {
|
||||
appSetting "consumerKey"
|
||||
appSetting "consumerSecret"
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// PAGES
|
||||
// ========================================================
|
||||
|
||||
preferences {
|
||||
page(name: "authPage")
|
||||
}
|
||||
|
||||
def authPage() {
|
||||
|
||||
def installOptions = false
|
||||
def description = "Required (tap to set)"
|
||||
def authState
|
||||
|
||||
if (oauth_token()) {
|
||||
// TODO: Check if it's valid
|
||||
if (true) {
|
||||
description = "Saved (tap to change)"
|
||||
installOptions = true
|
||||
authState = "complete"
|
||||
} else {
|
||||
// Worth differentiating here? (no longer valid vs. non-existent state.externalAuthToken?)
|
||||
description = "Required (tap to set)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
dynamicPage(name: "authPage", install: installOptions, uninstall: true) {
|
||||
section {
|
||||
|
||||
if (installOptions) {
|
||||
input(name: "withingsLabel", type: "text", title: "Add a name", description: null, required: true)
|
||||
}
|
||||
|
||||
href url: shortUrl("authenticate"), style: "embedded", required: false, title: "Authenticate with Withings", description: description, state: authState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// MAPPINGS
|
||||
// ========================================================
|
||||
|
||||
mappings {
|
||||
path("/authenticate") {
|
||||
action:
|
||||
[
|
||||
GET: "authenticate"
|
||||
]
|
||||
}
|
||||
path("/x") {
|
||||
action:
|
||||
[
|
||||
GET: "exchangeTokenFromWithings"
|
||||
]
|
||||
}
|
||||
path("/n") {
|
||||
action:
|
||||
[POST: "notificationReceived"]
|
||||
}
|
||||
|
||||
path("/test/:action") {
|
||||
action:
|
||||
[GET: "test"]
|
||||
}
|
||||
}
|
||||
|
||||
def test() {
|
||||
"${params.action}"()
|
||||
}
|
||||
|
||||
def authenticate() {
|
||||
// do not hit userAuthorizationUrl when the page is executed. It will replace oauth_tokens
|
||||
// instead, redirect through here so we know for sure that the user wants to authenticate
|
||||
// plus, the short-lived tokens that are used during authentication are only valid for 2 minutes
|
||||
// so make sure we give the user as much of that 2 minutes as possible to enter their credentials and deal with network latency
|
||||
log.trace "starting Withings authentication flow"
|
||||
redirect location: userAuthorizationUrl()
|
||||
}
|
||||
|
||||
def exchangeTokenFromWithings() {
|
||||
// Withings hits us here during the oAuth flow
|
||||
// log.trace "exchangeTokenFromWithings ${params}"
|
||||
atomicState.userid = params.userid // TODO: restructure this for multi-user access
|
||||
exchangeToken()
|
||||
}
|
||||
|
||||
def notificationReceived() {
|
||||
// log.trace "notificationReceived params: ${params}"
|
||||
|
||||
def notificationParams = [
|
||||
startdate: params.startdate,
|
||||
userid : params.userid,
|
||||
enddate : params.enddate,
|
||||
]
|
||||
|
||||
def measures = wGetMeasures(notificationParams)
|
||||
sendMeasureEvents(measures)
|
||||
return [status: 0]
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// HANDLERS
|
||||
// ========================================================
|
||||
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
// wRevokeAllNotifications()
|
||||
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
if (!getChild()) { createChild() }
|
||||
app.updateLabel(withingsLabel)
|
||||
wCreateNotification()
|
||||
backfillMeasures()
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// CHILD DEVICE
|
||||
// ========================================================
|
||||
|
||||
private getChild() {
|
||||
def children = childDevices
|
||||
children.size() ? children.first() : null
|
||||
}
|
||||
|
||||
private void createChild() {
|
||||
def child = addChildDevice("smartthings", "Withings User", userid(), null, [name: app.label, label: withingsLabel])
|
||||
atomicState.child = [dni: child.deviceNetworkId]
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// URL HELPERS
|
||||
// ========================================================
|
||||
|
||||
def stBaseUrl() {
|
||||
if (!atomicState.serverUrl) {
|
||||
stToken()
|
||||
atomicState.serverUrl = buildActionUrl("").split(/api\//).first()
|
||||
}
|
||||
return atomicState.serverUrl
|
||||
}
|
||||
|
||||
def stToken() {
|
||||
atomicState.accessToken ?: createAccessToken()
|
||||
}
|
||||
|
||||
def shortUrl(path = "", urlParams = [:]) {
|
||||
attachParams("${stBaseUrl()}api/t/${stToken()}/s/${app.id}/${path}", urlParams)
|
||||
}
|
||||
|
||||
def noTokenUrl(path = "", urlParams = [:]) {
|
||||
attachParams("${stBaseUrl()}api/smartapps/installations/${app.id}/${path}", urlParams)
|
||||
}
|
||||
|
||||
def attachParams(url, urlParams = [:]) {
|
||||
[url, toQueryString(urlParams)].findAll().join("?")
|
||||
}
|
||||
|
||||
String toQueryString(Map m = [:]) {
|
||||
// log.trace "toQueryString. URLEncoder will be used on ${m}"
|
||||
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// WITHINGS MEASURES
|
||||
// ========================================================
|
||||
|
||||
def unixTime(date = new Date()) {
|
||||
def unixTime = date.time / 1000 as int
|
||||
// log.debug "converting ${date.time} to ${unixTime}"
|
||||
unixTime
|
||||
}
|
||||
|
||||
def backfillMeasures() {
|
||||
// log.trace "backfillMeasures"
|
||||
def measureParams = [startdate: unixTime(new Date() - 10)]
|
||||
def measures = wGetMeasures(measureParams)
|
||||
sendMeasureEvents(measures)
|
||||
}
|
||||
|
||||
// this is body measures. // TODO: get activity and others too
|
||||
def wGetMeasures(measureParams = [:]) {
|
||||
def baseUrl = "https://wbsapi.withings.net/measure"
|
||||
def urlParams = [
|
||||
action : "getmeas",
|
||||
userid : userid(),
|
||||
startdate : unixTime(new Date() - 5),
|
||||
enddate : unixTime(),
|
||||
oauth_token: oauth_token()
|
||||
] + measureParams
|
||||
def measureData = fetchDataFromWithings(baseUrl, urlParams)
|
||||
// log.debug "measureData: ${measureData}"
|
||||
measureData.body.measuregrps.collect { parseMeasureGroup(it) }.flatten()
|
||||
}
|
||||
/*
|
||||
[
|
||||
body:[
|
||||
measuregrps:[
|
||||
[
|
||||
category:1, // 1 for real measurements, 2 for user objectives.
|
||||
grpid:310040317,
|
||||
measures:[
|
||||
[
|
||||
unit:0, // Power of ten the "value" parameter should be multiplied to to get the real value. Eg : value = 20 and unit=-1 means the value really is 2.0
|
||||
value:60, // Value for the measure in S.I units (kilogram, meters, etc.). Value should be multiplied by 10 to the power of "unit" (see below) to get the real value.
|
||||
type:11 // 1 : Weight (kg), 4 : Height (meter), 5 : Fat Free Mass (kg), 6 : Fat Ratio (%), 8 : Fat Mass Weight (kg), 9 : Diastolic Blood Pressure (mmHg), 10 : Systolic Blood Pressure (mmHg), 11 : Heart Pulse (bpm), 54 : SP02(%)
|
||||
],
|
||||
[
|
||||
unit:-3,
|
||||
value:-1000,
|
||||
type:18
|
||||
]
|
||||
],
|
||||
date:1422750210,
|
||||
attrib:2
|
||||
]
|
||||
],
|
||||
updatetime:1422750227
|
||||
],
|
||||
status:0
|
||||
]
|
||||
*/
|
||||
|
||||
def sendMeasureEvents(measures) {
|
||||
// log.debug "measures: ${measures}"
|
||||
measures.each {
|
||||
if (it.name && it.value) {
|
||||
sendEvent(userid(), it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def parseMeasureGroup(measureGroup) {
|
||||
long time = measureGroup.date // must be long. INT_MAX is too small
|
||||
time *= 1000
|
||||
measureGroup.measures.collect { parseMeasure(it) + [date: new Date(time)] }
|
||||
}
|
||||
|
||||
def parseMeasure(measure) {
|
||||
// log.debug "parseMeasure($measure)"
|
||||
[
|
||||
name : measureAttribute(measure),
|
||||
value: measureValue(measure)
|
||||
]
|
||||
}
|
||||
|
||||
def measureValue(measure) {
|
||||
def value = measure.value * 10.power(measure.unit)
|
||||
if (measure.type == 1) { // Weight (kg)
|
||||
value *= 2.20462262 // kg to lbs
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
String measureAttribute(measure) {
|
||||
def attribute = ""
|
||||
switch (measure.type) {
|
||||
case 1: attribute = "weight"; break;
|
||||
case 4: attribute = "height"; break;
|
||||
case 5: attribute = "leanMass"; break;
|
||||
case 6: attribute = "fatRatio"; break;
|
||||
case 8: attribute = "fatMass"; break;
|
||||
case 9: attribute = "diastolicPressure"; break;
|
||||
case 10: attribute = "systolicPressure"; break;
|
||||
case 11: attribute = "heartPulse"; break;
|
||||
case 54: attribute = "SP02"; break;
|
||||
}
|
||||
return attribute
|
||||
}
|
||||
|
||||
String measureDescription(measure) {
|
||||
def description = ""
|
||||
switch (measure.type) {
|
||||
case 1: description = "Weight (kg)"; break;
|
||||
case 4: description = "Height (meter)"; break;
|
||||
case 5: description = "Fat Free Mass (kg)"; break;
|
||||
case 6: description = "Fat Ratio (%)"; break;
|
||||
case 8: description = "Fat Mass Weight (kg)"; break;
|
||||
case 9: description = "Diastolic Blood Pressure (mmHg)"; break;
|
||||
case 10: description = "Systolic Blood Pressure (mmHg)"; break;
|
||||
case 11: description = "Heart Pulse (bpm)"; break;
|
||||
case 54: description = "SP02(%)"; break;
|
||||
}
|
||||
return description
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// WITHINGS NOTIFICATIONS
|
||||
// ========================================================
|
||||
|
||||
def wNotificationBaseUrl() { "https://wbsapi.withings.net/notify" }
|
||||
|
||||
def wNotificationCallbackUrl() { shortUrl("n") }
|
||||
|
||||
def wGetNotification() {
|
||||
def userId = userid()
|
||||
def url = wNotificationBaseUrl()
|
||||
def params = [
|
||||
action: "subscribe"
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
// TODO: keep track of notification expiration
|
||||
def wCreateNotification() {
|
||||
def baseUrl = wNotificationBaseUrl()
|
||||
def urlParams = [
|
||||
action : "subscribe",
|
||||
userid : userid(),
|
||||
callbackurl: wNotificationCallbackUrl(),
|
||||
oauth_token: oauth_token(),
|
||||
comment : "hmm" // TODO: figure out what to do here. spaces seem to break the request
|
||||
]
|
||||
|
||||
fetchDataFromWithings(baseUrl, urlParams)
|
||||
}
|
||||
|
||||
def wRevokeAllNotifications() {
|
||||
def notifications = wListNotifications()
|
||||
notifications.each {
|
||||
wRevokeNotification([callbackurl: it.callbackurl]) // use the callbackurl Withings has on file
|
||||
}
|
||||
}
|
||||
|
||||
def wRevokeNotification(notificationParams = [:]) {
|
||||
def baseUrl = wNotificationBaseUrl()
|
||||
def urlParams = [
|
||||
action : "revoke",
|
||||
userid : userid(),
|
||||
callbackurl: wNotificationCallbackUrl(),
|
||||
oauth_token: oauth_token()
|
||||
] + notificationParams
|
||||
|
||||
fetchDataFromWithings(baseUrl, urlParams)
|
||||
}
|
||||
|
||||
def wListNotifications() {
|
||||
|
||||
/*
|
||||
{
|
||||
body: {
|
||||
profiles: [
|
||||
{
|
||||
appli: 1,
|
||||
expires: 2147483647,
|
||||
callbackurl: "https://graph.api.smartthings.com/api/t/72ab3e57-5839-4cca-9562-dcc818f83bc9/s/537757a0-c4c8-40ea-8cea-aa283915bbd9/n",
|
||||
comment: "hmm"
|
||||
}
|
||||
]
|
||||
},
|
||||
status: 0
|
||||
}*/
|
||||
|
||||
def baseUrl = wNotificationBaseUrl()
|
||||
def urlParams = [
|
||||
action : "list",
|
||||
userid : userid(),
|
||||
callbackurl: wNotificationCallbackUrl(),
|
||||
oauth_token: oauth_token()
|
||||
]
|
||||
|
||||
def notificationData = fetchDataFromWithings(baseUrl, urlParams)
|
||||
notificationData.body.profiles
|
||||
}
|
||||
|
||||
def defaultOauthParams() {
|
||||
defaultParameterKeys().inject([:]) { keyMap, currentKey ->
|
||||
keyMap[currentKey] = "${currentKey}"()
|
||||
keyMap
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// WITHINGS DATA FETCHING
|
||||
// ========================================================
|
||||
|
||||
def fetchDataFromWithings(baseUrl, urlParams) {
|
||||
|
||||
// log.debug "fetchDataFromWithings(${baseUrl}, ${urlParams})"
|
||||
|
||||
def defaultParams = defaultOauthParams()
|
||||
def paramStrings = buildOauthParams(urlParams + defaultParams)
|
||||
// log.debug "paramStrings: $paramStrings"
|
||||
def url = buildOauthUrl(baseUrl, paramStrings, oauth_token_secret())
|
||||
def json
|
||||
// log.debug "about to make request to ${url}"
|
||||
httpGet(uri: url, headers: ["Content-Type": "application/json"]) { response ->
|
||||
json = new groovy.json.JsonSlurper().parse(response.data)
|
||||
}
|
||||
return json
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// WITHINGS OAUTH LOGGING
|
||||
// ========================================================
|
||||
|
||||
def wLogEnabled() { false } // For troubleshooting Oauth flow
|
||||
|
||||
void wLog(message = "") {
|
||||
if (!wLogEnabled()) { return }
|
||||
def wLogMessage = atomicState.wLogMessage
|
||||
if (wLogMessage.length()) {
|
||||
wLogMessage += "\n|"
|
||||
}
|
||||
wLogMessage += message
|
||||
atomicState.wLogMessage = wLogMessage
|
||||
}
|
||||
|
||||
void wLogNew(seedMessage = "") {
|
||||
if (!wLogEnabled()) { return }
|
||||
def olMessage = atomicState.wLogMessage
|
||||
if (oldMessage) {
|
||||
log.debug "purging old wLogMessage: ${olMessage}"
|
||||
}
|
||||
atomicState.wLogMessage = seedMessage
|
||||
}
|
||||
|
||||
String wLogMessage() {
|
||||
if (!wLogEnabled()) { return }
|
||||
def wLogMessage = atomicState.wLogMessage
|
||||
atomicState.wLogMessage = ""
|
||||
wLogMessage
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// WITHINGS OAUTH DESCRIPTION
|
||||
// >>>>>> The user opens the authPage for this SmartApp
|
||||
// STEP 1 get a token to be used in the url the user taps
|
||||
// STEP 2 generate the url to be tapped by the user
|
||||
// >>>>>> The user taps the url and logs in to Withings
|
||||
// STEP 3 generate a token to be used for accessing user data
|
||||
// STEP 4 access user data
|
||||
// ========================================================
|
||||
|
||||
// ========================================================
|
||||
// WITHINGS OAUTH STEP 1: get an oAuth "request token"
|
||||
// ========================================================
|
||||
|
||||
def requestTokenUrl() {
|
||||
wLogNew "WITHINGS OAUTH STEP 1: get an oAuth 'request token'"
|
||||
|
||||
def keys = defaultParameterKeys() + "oauth_callback"
|
||||
def paramStrings = buildOauthParams(keys.sort())
|
||||
|
||||
buildOauthUrl("https://oauth.withings.com/account/request_token", paramStrings, "")
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// WITHINGS OAUTH STEP 2: End-user authorization
|
||||
// ========================================================
|
||||
|
||||
def userAuthorizationUrl() {
|
||||
|
||||
// get url from Step 1
|
||||
def tokenUrl = requestTokenUrl()
|
||||
|
||||
// collect token from Withings
|
||||
collectTokenFromWithings(tokenUrl)
|
||||
|
||||
wLogNew "WITHINGS OAUTH STEP 2: End-user authorization"
|
||||
|
||||
def keys = defaultParameterKeys() + "oauth_token"
|
||||
def paramStrings = buildOauthParams(keys.sort())
|
||||
|
||||
buildOauthUrl("https://oauth.withings.com/account/authorize", paramStrings, oauth_token_secret())
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// WITHINGS OAUTH STEP 3: Generating access token
|
||||
// ========================================================
|
||||
|
||||
def exchangeTokenUrl() {
|
||||
wLogNew "WITHINGS OAUTH STEP 3: Generating access token"
|
||||
|
||||
def keys = defaultParameterKeys() + ["oauth_token", "userid"]
|
||||
def paramStrings = buildOauthParams(keys.sort())
|
||||
|
||||
buildOauthUrl("https://oauth.withings.com/account/access_token", paramStrings, oauth_token_secret())
|
||||
}
|
||||
|
||||
def exchangeToken() {
|
||||
|
||||
def tokenUrl = exchangeTokenUrl()
|
||||
// log.debug "about to hit ${tokenUrl}"
|
||||
|
||||
try {
|
||||
// replace old token with a long-lived token
|
||||
def token = collectTokenFromWithings(tokenUrl)
|
||||
// log.debug "collected token from Withings: ${token}"
|
||||
renderAction("authorized", "Withings Connection")
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.error e
|
||||
renderAction("notAuthorized", "Withings Connection Failed")
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// OAUTH 1.0
|
||||
// ========================================================
|
||||
|
||||
def defaultParameterKeys() {
|
||||
[
|
||||
"oauth_consumer_key",
|
||||
"oauth_nonce",
|
||||
"oauth_signature_method",
|
||||
"oauth_timestamp",
|
||||
"oauth_version"
|
||||
]
|
||||
}
|
||||
|
||||
def oauth_consumer_key() { consumerKey }
|
||||
|
||||
def oauth_nonce() { nonce() }
|
||||
|
||||
def nonce() { UUID.randomUUID().toString().replaceAll("-", "") }
|
||||
|
||||
def oauth_signature_method() { "HMAC-SHA1" }
|
||||
|
||||
def oauth_timestamp() { (int) (new Date().time / 1000) }
|
||||
|
||||
def oauth_version() { 1.0 }
|
||||
|
||||
def oauth_callback() { shortUrl("x") }
|
||||
|
||||
def oauth_token() { atomicState.wToken?.oauth_token }
|
||||
|
||||
def oauth_token_secret() { atomicState.wToken?.oauth_token_secret }
|
||||
|
||||
def userid() { atomicState.userid }
|
||||
|
||||
String hmac(String oAuthSignatureBaseString, String oAuthSecret) throws java.security.SignatureException {
|
||||
if (!oAuthSecret.contains("&")) { log.warn "Withings requires \"&\" to be included no matter what" }
|
||||
// get an hmac_sha1 key from the raw key bytes
|
||||
def signingKey = new javax.crypto.spec.SecretKeySpec(oAuthSecret.getBytes(), "HmacSHA1")
|
||||
// get an hmac_sha1 Mac instance and initialize with the signing key
|
||||
def mac = javax.crypto.Mac.getInstance("HmacSHA1")
|
||||
mac.init(signingKey)
|
||||
// compute the hmac on input data bytes
|
||||
byte[] rawHmac = mac.doFinal(oAuthSignatureBaseString.getBytes())
|
||||
return org.apache.commons.codec.binary.Base64.encodeBase64String(rawHmac)
|
||||
}
|
||||
|
||||
Map parseResponseString(String responseString) {
|
||||
// log.debug "parseResponseString: ${responseString}"
|
||||
responseString.split("&").inject([:]) { c, it ->
|
||||
def parts = it.split('=')
|
||||
def k = parts[0]
|
||||
def v = parts[1]
|
||||
c[k] = v
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
String applyParams(endpoint, oauthParams) { endpoint + "?" + oauthParams.sort().join("&") }
|
||||
|
||||
String buildSignature(endpoint, oAuthParams, oAuthSecret) {
|
||||
def oAuthSignatureBaseParts = ["GET", endpoint, oAuthParams.join("&")]
|
||||
def oAuthSignatureBaseString = oAuthSignatureBaseParts.collect { URLEncoder.encode(it) }.join("&")
|
||||
wLog " ==> oAuth signature base string : \n${oAuthSignatureBaseString}"
|
||||
wLog " .. applying hmac-sha1 to base string, with secret : ${oAuthSecret} (notice the \"&\")"
|
||||
wLog " .. base64 encode then url-encode the hmac-sha1 hash"
|
||||
String hmacResult = hmac(oAuthSignatureBaseString, oAuthSecret)
|
||||
def signature = URLEncoder.encode(hmacResult)
|
||||
wLog " ==> oauth_signature = ${signature}"
|
||||
return signature
|
||||
}
|
||||
|
||||
List buildOauthParams(List parameterKeys) {
|
||||
wLog " .. adding oAuth parameters : "
|
||||
def oauthParams = []
|
||||
parameterKeys.each { key ->
|
||||
def value = "${key}"()
|
||||
wLog " ${key} = ${value}"
|
||||
oauthParams << "${key}=${URLEncoder.encode(value.toString())}"
|
||||
}
|
||||
|
||||
wLog " .. sorting all request parameters alphabetically "
|
||||
oauthParams.sort()
|
||||
}
|
||||
|
||||
List buildOauthParams(Map parameters) {
|
||||
wLog " .. adding oAuth parameters : "
|
||||
def oauthParams = []
|
||||
parameters.each { k, v ->
|
||||
wLog " ${k} = ${v}"
|
||||
oauthParams << "${k}=${URLEncoder.encode(v.toString())}"
|
||||
}
|
||||
|
||||
wLog " .. sorting all request parameters alphabetically "
|
||||
oauthParams.sort()
|
||||
}
|
||||
|
||||
String buildOauthUrl(String endpoint, List parameterStrings, String oAuthTokenSecret) {
|
||||
wLog "Api endpoint : ${endpoint}"
|
||||
|
||||
wLog "Signing request :"
|
||||
def oAuthSecret = "${consumerSecret}&${oAuthTokenSecret}"
|
||||
def signature = buildSignature(endpoint, parameterStrings, oAuthSecret)
|
||||
|
||||
parameterStrings << "oauth_signature=${signature}"
|
||||
|
||||
def finalUrl = applyParams(endpoint, parameterStrings)
|
||||
wLog "Result: ${finalUrl}"
|
||||
if (wLogEnabled()) {
|
||||
log.debug wLogMessage()
|
||||
}
|
||||
return finalUrl
|
||||
}
|
||||
|
||||
def collectTokenFromWithings(tokenUrl) {
|
||||
// get token from Withings using the url generated in Step 1
|
||||
def tokenString
|
||||
httpGet(uri: tokenUrl) { resp -> // oauth_token=<token_key>&oauth_token_secret=<token_secret>
|
||||
tokenString = resp.data.toString()
|
||||
// log.debug "collectTokenFromWithings: ${tokenString}"
|
||||
}
|
||||
def token = parseResponseString(tokenString)
|
||||
atomicState.wToken = token
|
||||
return token
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// APP SETTINGS
|
||||
// ========================================================
|
||||
|
||||
def getConsumerKey() { appSettings.consumerKey }
|
||||
|
||||
def getConsumerSecret() { appSettings.consumerSecret }
|
||||
|
||||
// figure out how to put this in settings
|
||||
def getUserId() { atomicState.wToken?.userid }
|
||||
|
||||
// ========================================================
|
||||
// HTML rendering
|
||||
// ========================================================
|
||||
|
||||
def renderAction(action, title = "") {
|
||||
log.debug "renderAction: $action"
|
||||
renderHTML(title) {
|
||||
head { "${action}HtmlHead"() }
|
||||
body { "${action}HtmlBody"() }
|
||||
}
|
||||
}
|
||||
|
||||
def authorizedHtmlHead() {
|
||||
log.trace "authorizedHtmlHead"
|
||||
"""
|
||||
<style type="text/css">
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
.container {
|
||||
/*width: 560px;
|
||||
padding: 40px;*/
|
||||
/*background: #eee;*/
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
vertical-align: middle;
|
||||
max-width:20%;
|
||||
}
|
||||
img:nth-child(2) {
|
||||
margin: 0 30px;
|
||||
}
|
||||
p {
|
||||
/*font-size: 1.2em;*/
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
text-align: center;
|
||||
color: #666666;
|
||||
padding: 0 10px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
/*
|
||||
p:last-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
*/
|
||||
span {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
}
|
||||
|
||||
def authorizedHtmlBody() {
|
||||
"""
|
||||
<div class="container">
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/withings@2x.png" alt="withings icon" />
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected device icon" />
|
||||
<img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings logo" />
|
||||
<p>Your Withings scale is now connected to SmartThings!</p>
|
||||
<p>Click 'Done' to finish setup.</p>
|
||||
</div>
|
||||
"""
|
||||
}
|
||||
|
||||
def notAuthorizedHtmlHead() {
|
||||
log.trace "notAuthorizedHtmlHead"
|
||||
authorizedHtmlHead()
|
||||
}
|
||||
|
||||
def notAuthorizedHtmlBody() {
|
||||
"""
|
||||
<div class="container">
|
||||
<p>There was an error connecting to SmartThings!</p>
|
||||
<p>Click 'Done' to try again.</p>
|
||||
</div>
|
||||
"""
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
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