mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-04-05 06:13:08 +01:00
Compare commits
233 Commits
PROD_2016.
...
PROD_2016.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6beb8bb50c | ||
|
|
3675332b75 | ||
|
|
7431346187 | ||
|
|
6aa0ff97b3 | ||
|
|
0a040aa51b | ||
|
|
58d8a7dac5 | ||
|
|
a98d3dc2d6 | ||
|
|
2d25a0e63f | ||
|
|
633a179074 | ||
|
|
49bc42b4ab | ||
|
|
d258c46aee | ||
|
|
add519433c | ||
|
|
f6dcaf6d09 | ||
|
|
b07b34f66c | ||
|
|
96659f0a73 | ||
|
|
699f80e9f7 | ||
|
|
d21dfc09fe | ||
|
|
d196125092 | ||
|
|
44088d626a | ||
|
|
2966c4d5a1 | ||
|
|
3343273d40 | ||
|
|
5c70da54a4 | ||
|
|
687c64d29d | ||
|
|
c9d1b168f7 | ||
|
|
2f87309fdf | ||
|
|
37524f17b2 | ||
|
|
47522facc7 | ||
|
|
4363661157 | ||
|
|
330b41941a | ||
|
|
26d286e0a0 | ||
|
|
ef2323f1b1 | ||
|
|
51452bc095 | ||
|
|
b7b29d8dbc | ||
|
|
b8111e8760 | ||
|
|
24ea8269a3 | ||
|
|
20df244dca | ||
|
|
583d42df13 | ||
|
|
f1309b2ee2 | ||
|
|
ec1ae2d0b1 | ||
|
|
5e48e710d4 | ||
|
|
07c5a3533f | ||
|
|
72b2016b7d | ||
|
|
a9aee8fd96 | ||
|
|
5c015cf678 | ||
|
|
cf1a46e309 | ||
|
|
7c5438880d | ||
|
|
d9888b3184 | ||
|
|
b582c3d832 | ||
|
|
1ff77dc608 | ||
|
|
afbec02217 | ||
|
|
434a72bd13 | ||
|
|
3fba7c9422 | ||
|
|
b63d4a9156 | ||
|
|
6eb29ad019 | ||
|
|
711cdc3ebf | ||
|
|
f58a1ef589 | ||
|
|
db5237ca33 | ||
|
|
7791c68a8a | ||
|
|
db4140ffd6 | ||
|
|
c15b1e88e1 | ||
|
|
ac422076c8 | ||
|
|
94f57dd249 | ||
|
|
c11c146690 | ||
|
|
9a5d506668 | ||
|
|
723ef7e7e6 | ||
|
|
84c72de640 | ||
|
|
570454e6c3 | ||
|
|
a5d95fb025 | ||
|
|
b12df3f360 | ||
|
|
50696902cf | ||
|
|
409658e899 | ||
|
|
1068a553f5 | ||
|
|
bbdf9ff02a | ||
|
|
9dac541473 | ||
|
|
a6cc506803 | ||
|
|
aba8a7ad4b | ||
|
|
b4c912ab80 | ||
|
|
f5b7dfd4eb | ||
|
|
3e0306e912 | ||
|
|
2c2d75ae37 | ||
|
|
61ef40831c | ||
|
|
19169748df | ||
|
|
0f5a2c5e21 | ||
|
|
6dbb61536b | ||
|
|
84323afa04 | ||
|
|
04a7627c21 | ||
|
|
12b09acfa8 | ||
|
|
9e8ad0dfdf | ||
|
|
80eb1e43b9 | ||
|
|
af383de368 | ||
|
|
427fa88ed8 | ||
|
|
57514944d5 | ||
|
|
823efed562 | ||
|
|
540db429f3 | ||
|
|
0c3a5de661 | ||
|
|
989f08708b | ||
|
|
60e09c56b7 | ||
|
|
9f5eb7b85a | ||
|
|
62b37f5c3d | ||
|
|
64e4ccc517 | ||
|
|
c17830ab56 | ||
|
|
aa890ae3d5 | ||
|
|
8d701b9fea | ||
|
|
c7f78a69e4 | ||
|
|
80500207a8 | ||
|
|
29db335e1c | ||
|
|
55b5b7d03d | ||
|
|
730ccccd45 | ||
|
|
6a1a2b0ed9 | ||
|
|
719b24ecd6 | ||
|
|
9d5ab3bfc8 | ||
|
|
aae7f23a22 | ||
|
|
218cc43520 | ||
|
|
5b0ca4b815 | ||
|
|
9ddc020f04 | ||
|
|
aab3b8d7f8 | ||
|
|
a0ccf35eaa | ||
|
|
9fbbaec8f6 | ||
|
|
e4c1824afd | ||
|
|
797a58cb68 | ||
|
|
c428267d63 | ||
|
|
02f30cf425 | ||
|
|
fea802ffce | ||
|
|
6400d26f4a | ||
|
|
5e3aaa3270 | ||
|
|
f5c3997679 | ||
|
|
81cf1179ef | ||
|
|
7113d7470e | ||
|
|
79d20b0edb | ||
|
|
b6d862fdd4 | ||
|
|
d58084c438 | ||
|
|
dbfaef3e69 | ||
|
|
40ed88e7fd | ||
|
|
1d6e22dc16 | ||
|
|
30993aa218 | ||
|
|
2f8ed277ff | ||
|
|
1d180ac487 | ||
|
|
230541a145 | ||
|
|
8c4f7edc83 | ||
|
|
4f188581df | ||
|
|
71880e2644 | ||
|
|
0b7bb40474 | ||
|
|
8d920ea072 | ||
|
|
e373b6f92e | ||
|
|
43a1ae6371 | ||
|
|
60a98e3074 | ||
|
|
a441b94a33 | ||
|
|
ced03d746d | ||
|
|
5341d0d06f | ||
|
|
2a58d7ff62 | ||
|
|
260917d515 | ||
|
|
c1478d3e96 | ||
|
|
8b9bff15dc | ||
|
|
75c1ede16c | ||
|
|
a7acc384a2 | ||
|
|
c6998e5f1d | ||
|
|
f95e906d6e | ||
|
|
a6c7ab49b6 | ||
|
|
4891e3b947 | ||
|
|
ae91f9bff5 | ||
|
|
bb87ad2cf0 | ||
|
|
5dff03fb69 | ||
|
|
629d768575 | ||
|
|
dd7c6b90d5 | ||
|
|
4523498dab | ||
|
|
e89e45e000 | ||
|
|
78ec280e83 | ||
|
|
1f144d36e4 | ||
|
|
f5bd580c9e | ||
|
|
d5329dbde3 | ||
|
|
48818cfb06 | ||
|
|
079919260b | ||
|
|
570922e2ac | ||
|
|
53ed9b4d2b | ||
|
|
7149a81c85 | ||
|
|
30274f0cd7 | ||
|
|
8869cd3af0 | ||
|
|
fb0caa6446 | ||
|
|
3d05d42cb8 | ||
|
|
3184615e87 | ||
|
|
0f70362e0a | ||
|
|
bc817f8530 | ||
|
|
01b8399893 | ||
|
|
81318bafac | ||
|
|
60c2006bfc | ||
|
|
1e4f1223e7 | ||
|
|
b78bce55b2 | ||
|
|
01593c3973 | ||
|
|
6862785d6c | ||
|
|
763d7411e2 | ||
|
|
c703543f36 | ||
|
|
5db6ecda3e | ||
|
|
43b836f413 | ||
|
|
006b5e7bea | ||
|
|
62c8c19805 | ||
|
|
48e9a4bd6a | ||
|
|
07a4c0decc | ||
|
|
6aa09bb052 | ||
|
|
2f889de11a | ||
|
|
5584020e96 | ||
|
|
4ef2e694c2 | ||
|
|
826993cc45 | ||
|
|
ae3306928b | ||
|
|
8777ec5f6d | ||
|
|
91eb59a10d | ||
|
|
324ac13afb | ||
|
|
878eb66b8b | ||
|
|
8dfc270c2d | ||
|
|
614573a15c | ||
|
|
9c7b0875ba | ||
|
|
7568cbf781 | ||
|
|
1858c280a5 | ||
|
|
6b7d0968f6 | ||
|
|
159d3acf4f | ||
|
|
e8101630a3 | ||
|
|
f5ba78b221 | ||
|
|
19b8a7eeb9 | ||
|
|
1b37d649a5 | ||
|
|
dedb0f8465 | ||
|
|
06acc13575 | ||
|
|
c051d719cc | ||
|
|
1e54b93b0c | ||
|
|
bac37f9ca2 | ||
|
|
f0ecb65c09 | ||
|
|
1c0ddd2571 | ||
|
|
b7e0cbda09 | ||
|
|
f80e094bd9 | ||
|
|
fe2fbc3b97 | ||
|
|
383f72580a | ||
|
|
090a306939 | ||
|
|
5e6b4f74e0 | ||
|
|
4ad0a6fd9d | ||
|
|
22185c5440 |
30
build.gradle
30
build.gradle
@@ -19,7 +19,7 @@ buildscript {
|
||||
username smartThingsArtifactoryUserName
|
||||
password smartThingsArtifactoryPassword
|
||||
}
|
||||
url "http://artifactory.smartthings.com/libs-release-local"
|
||||
url "https://artifactory.smartthings.com/libs-release-local"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,9 +27,37 @@ buildscript {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
jcenter()
|
||||
maven {
|
||||
credentials {
|
||||
username smartThingsArtifactoryUserName
|
||||
password smartThingsArtifactoryPassword
|
||||
}
|
||||
url "https://artifactory.smartthings.com/libs-release-local"
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
devicetypes {
|
||||
groovy {
|
||||
srcDirs = ['devicetypes']
|
||||
}
|
||||
}
|
||||
smartapps {
|
||||
groovy {
|
||||
srcDirs = ['smartapps']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
devicetypesCompile 'org.codehaus.groovy:groovy-all:2.4.7'
|
||||
devicetypesCompile 'smartthings:appengine-z-wave:0.1.2'
|
||||
devicetypesCompile 'smartthings:appengine-zigbee:0.1.11'
|
||||
smartappsCompile 'org.codehaus.groovy:groovy-all:2.4.7'
|
||||
smartappsCompile 'smartthings:appengine-common:0.1.8'
|
||||
smartappsCompile 'org.codehaus.groovy.modules.http-builder:http-builder:0.7.1'
|
||||
smartappsCompile 'org.grails:grails-web:2.3.11'
|
||||
smartappsCompile 'org.json:json:20140107'
|
||||
}
|
||||
|
||||
slackSendMessage {
|
||||
|
||||
@@ -5,7 +5,9 @@ machine:
|
||||
|
||||
dependencies:
|
||||
override:
|
||||
- echo "Nothing to do."
|
||||
- ./gradlew dependencies -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD"
|
||||
post:
|
||||
- ./gradlew compileSmartappsGroovy compileDevicetypesGroovy -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD"
|
||||
|
||||
test:
|
||||
override:
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Netatmo Additional Module", namespace: "dianoga", author: "Brian Steere") {
|
||||
capability "Sensor"
|
||||
capability "Relative Humidity Measurement"
|
||||
capability "Temperature Measurement"
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Netatmo Basestation", namespace: "dianoga", author: "Brian Steere") {
|
||||
capability "Sensor"
|
||||
capability "Relative Humidity Measurement"
|
||||
capability "Temperature Measurement"
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Netatmo Outdoor Module", namespace: "dianoga", author: "Brian Steere") {
|
||||
capability "Sensor"
|
||||
capability "Relative Humidity Measurement"
|
||||
capability "Temperature Measurement"
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Netatmo Rain", namespace: "dianoga", author: "Brian Steere") {
|
||||
capability "Sensor"
|
||||
|
||||
attribute "rain", "number"
|
||||
attribute "rainSumHour", "number"
|
||||
attribute "rainSumDay", "number"
|
||||
|
||||
@@ -33,8 +33,8 @@ metadata {
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"FGMS", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app
|
||||
tileAttribute("device.motion", key:"PRIMARY_CONTROL") {
|
||||
attributeState("inactive", icon:"st.motion.motion.inactive", backgroundColor:"#79b821")
|
||||
attributeState("active", icon:"st.motion.motion.active", backgroundColor:"#ffa81e")
|
||||
attributeState("inactive", label:"no motion", icon:"st.motion.motion.inactive", backgroundColor:"#79b821")
|
||||
attributeState("active", label:"motion", icon:"st.motion.motion.active", backgroundColor:"#ffa81e")
|
||||
}
|
||||
|
||||
tileAttribute("device.tamper", key:"SECONDARY_CONTROL") {
|
||||
@@ -127,9 +127,10 @@ def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelR
|
||||
def map = [ displayed: true ]
|
||||
switch (cmd.sensorType) {
|
||||
case 1:
|
||||
def cmdScale = cmd.scale == 1 ? "F" : "C"
|
||||
map.name = "temperature"
|
||||
map.unit = cmd.scale == 1 ? "F" : "C"
|
||||
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, map.unit, cmd.precision)
|
||||
map.unit = getTemperatureScale()
|
||||
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision)
|
||||
break
|
||||
case 3:
|
||||
map.name = "illuminance"
|
||||
|
||||
@@ -87,16 +87,27 @@ def beep() {
|
||||
up to this long from the time you send the message to the time you hear a sound.
|
||||
*/
|
||||
|
||||
// Used source endpoint of 0x02 because we are using smartthings manufacturer specific cluster.
|
||||
[
|
||||
"raw 0xFC05 {15 0A 11 00 00 15 01}",
|
||||
"delay 200",
|
||||
"send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId",
|
||||
"delay 7000",
|
||||
"raw 0xFC05 {15 0A 11 00 00 15 01}",
|
||||
"delay 200",
|
||||
"send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId",
|
||||
"delay 7000",
|
||||
"raw 0xFC05 {15 0A 11 00 00 15 01}",
|
||||
"delay 200",
|
||||
"send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId",
|
||||
"delay 7000",
|
||||
"raw 0xFC05 {15 0A 11 00 00 15 01}",
|
||||
"delay 200",
|
||||
"send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId",
|
||||
"delay 7000",
|
||||
"raw 0xFC05 {15 0A 11 00 00 15 01}"
|
||||
"raw 0xFC05 {15 0A 11 00 00 15 01}",
|
||||
"delay 200",
|
||||
"send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId",
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -28,17 +28,6 @@ metadata {
|
||||
|
||||
}
|
||||
|
||||
// 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") {
|
||||
@@ -54,11 +43,9 @@ metadata {
|
||||
attributeState "power", label:'${currentValue} W'
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main "switch"
|
||||
details(["switch", "refresh"])
|
||||
}
|
||||
@@ -66,81 +53,44 @@ metadata {
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "Parse description $description"
|
||||
def name = null
|
||||
def value = null
|
||||
if (description?.startsWith("catchall:")) {
|
||||
def msg = zigbee.parse(description)
|
||||
log.trace msg
|
||||
log.trace "data: $msg.data"
|
||||
} else if (description?.startsWith("read attr -")) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
log.debug "Read attr: $description"
|
||||
if (descMap.cluster == "0006" && descMap.attrId == "0000") {
|
||||
name = "switch"
|
||||
value = descMap.value.endsWith("01") ? "on" : "off"
|
||||
} else {
|
||||
def reportValue = description.split(",").find {it.split(":")[0].trim() == "value"}?.split(":")[1].trim()
|
||||
name = "power"
|
||||
// assume 16 bit signed for encoding and power divisor is 10
|
||||
value = Integer.parseInt(reportValue, 16) / 10
|
||||
}
|
||||
} else if (description?.startsWith("on/off:")) {
|
||||
log.debug "Switch command"
|
||||
name = "switch"
|
||||
value = description?.endsWith(" 1") ? "on" : "off"
|
||||
}
|
||||
log.debug "description is $description"
|
||||
|
||||
def result = createEvent(name: name, value: value)
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
def event = zigbee.getEvent(description)
|
||||
if (event) {
|
||||
log.info event
|
||||
if (event.name == "power") {
|
||||
if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power
|
||||
event.value = (event.value as Integer) / 10 //TODO: The divisor value needs to be set as part of configuration
|
||||
sendEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
// Commands to device
|
||||
def on() {
|
||||
'zcl on-off on'
|
||||
else {
|
||||
sendEvent(event)
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug zigbee.parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
'zcl on-off off'
|
||||
zigbee.off()
|
||||
}
|
||||
|
||||
def on() {
|
||||
zigbee.on()
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
log.trace "setLevel($value)"
|
||||
sendEvent(name: "level", value: value)
|
||||
def level = hexString(Math.round(value * 255/100))
|
||||
def cmd = "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} 2000}"
|
||||
log.debug cmd
|
||||
cmd
|
||||
}
|
||||
|
||||
def meter() {
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0xB04 0x50B"
|
||||
zigbee.setLevel(value)
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0xB04 0x50B"
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0xB04 {${device.zigbeeId}} {}"
|
||||
]
|
||||
}
|
||||
|
||||
private hex(value, width=2) {
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() < width) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
refresh()
|
||||
}
|
||||
|
||||
@@ -23,10 +23,8 @@ Works with:
|
||||
|
||||
## Device Health
|
||||
|
||||
A Category C6 Connected Cree LED Bulb with maxReportTime of 10 min.
|
||||
Check-in interval is double the value of maxReportTime for Zigbee device.
|
||||
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||
Check-in interval = 2*10 = 20 min
|
||||
A Category C6 Connected Cree LED Bulb with maxReportTime of 5 mins.
|
||||
Check-in interval = 12 mins
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ metadata {
|
||||
|
||||
capability "Actuator"
|
||||
capability "Configuration"
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
@@ -67,12 +66,6 @@ def parse(String description) {
|
||||
def resultMap = zigbee.getEvent(description)
|
||||
if (resultMap) {
|
||||
sendEvent(resultMap)
|
||||
// Temporary fix for the case when Device is OFFLINE and is connected again
|
||||
if (state.lastActivity == null){
|
||||
state.lastActivity = now()
|
||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
||||
}
|
||||
state.lastActivity = now()
|
||||
}
|
||||
else {
|
||||
log.debug "DID NOT PARSE MESSAGE for description : $description"
|
||||
@@ -96,27 +89,23 @@ def setLevel(value) {
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
|
||||
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
|
||||
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
|
||||
state.lastActivity = null
|
||||
return zigbee.levelRefresh()
|
||||
} else {
|
||||
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
|
||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
||||
}
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig()
|
||||
}
|
||||
|
||||
def poll() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
sendEvent(name: "checkInterval", value: 1200, displayed: false, data: [protocol: "zigbee"])
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
def healthPoll() {
|
||||
log.debug "healthPoll()"
|
||||
def cmds = zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
cmds.each{ sendHubCommand(new physicalgraph.device.HubAction(it))}
|
||||
}
|
||||
|
||||
def configure() {
|
||||
unschedule()
|
||||
runEvery5Minutes("healthPoll")
|
||||
// Device-Watch allows 2 check-in misses from device
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
}
|
||||
|
||||
2
devicetypes/smartthings/dimmer-switch.src/.st-ignore
Normal file
2
devicetypes/smartthings/dimmer-switch.src/.st-ignore
Normal file
@@ -0,0 +1,2 @@
|
||||
.st-ignore
|
||||
README.md
|
||||
45
devicetypes/smartthings/dimmer-switch.src/README.md
Normal file
45
devicetypes/smartthings/dimmer-switch.src/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Z-wave Dimmer Switch
|
||||
|
||||
|
||||
|
||||
Works with:
|
||||
|
||||
* [GE Z-Wave In-Wall Smart Dimmer (GE 12724)](http://products.z-wavealliance.org/products/1197)
|
||||
* [GE Z-Wave In-Wall Smart Dimmer (Toggle) (GE 12729)](http://products.z-wavealliance.org/products/1201)
|
||||
* [GE Z-Wave Plug-in Smart Dimmer (GE 12718)](http://products.z-wavealliance.org/products/1191)
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Capabilities](#capabilities)
|
||||
* [Health](#device-health)
|
||||
* [Troubleshooting](#Troubleshooting)
|
||||
|
||||
## Capabilities
|
||||
|
||||
* **Switch Level** - it's defined to accept two parameters, the level and the rate of dimming
|
||||
* **Actuator** - represents that a Device has commands
|
||||
* **Indicator** - gives you the ability to set the indicator LED light on a Z-Wave switch
|
||||
* **Switch** - can detect state (possible values: on/off)
|
||||
* **Polling** - represents that poll() can be implemented for the device
|
||||
* **Refresh** - _refresh()_ command for status updates
|
||||
* **Sensor** - detects sensor events
|
||||
* **Health Check** - indicates ability to get device health notifications
|
||||
|
||||
## Device Health
|
||||
|
||||
Z-Wave Smart Dimmers (In-Wall, In-Wall(Toggle), Plug-In) are polled by the hub.
|
||||
As of hubCore version 0.14.38 the hub sends up reports every 15 minutes regardless of whether the state changed.
|
||||
Check-in interval = 32 mins.
|
||||
Not to mention after going OFFLINE when the device is plugged back in, it might take a considerable amount of time for
|
||||
the device to appear as ONLINE again. This is because if this listening device does not respond to two poll requests in a row,
|
||||
it is not polled for 5 minutes by the hub. This can delay up the process of being marked ONLINE by quite some time.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range.
|
||||
Pairing needs to be tried again by placing the device closer to the hub.
|
||||
Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link:
|
||||
* [General Z-Wave Dimmer/Switch Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/200955890-Troubleshooting-GE-in-wall-switch-or-dimmer-won-t-respond-to-commands-or-automations-Z-Wave-)
|
||||
* [GE Z-Wave In-Wall Smart Dimmer (GE 12724) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/200902600-GE-In-Wall-Paddle-Dimmer-Switch-GE-12724-Z-Wave-)
|
||||
* [GE Z-Wave In-Wall Smart Dimmer (Toggle) (GE 12729) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/207568463-GE-In-Wall-Smart-Toggle-Dimmer-GE-12729-Z-Wave-)
|
||||
* [GE Z-Wave Plug-in Smart Dimmer (GE 12718) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/202088474-GE-Plug-In-Smart-Dimmer-GE-12718-Z-Wave-)
|
||||
@@ -20,6 +20,7 @@ metadata {
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Health Check"
|
||||
|
||||
fingerprint mfr:"0063", prod:"4457", deviceJoinName: "Z-Wave Wall Dimmer"
|
||||
fingerprint mfr:"0063", prod:"4944", deviceJoinName: "Z-Wave Wall Dimmer"
|
||||
@@ -82,6 +83,8 @@ metadata {
|
||||
}
|
||||
|
||||
def updated(){
|
||||
// Device-Watch simply pings if no device events received for 32min(checkInterval)
|
||||
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
|
||||
switch (ledIndicator) {
|
||||
case "on":
|
||||
indicatorWhenOn()
|
||||
@@ -215,6 +218,13 @@ def poll() {
|
||||
zwave.switchMultilevelV1.switchMultilevelGet().format()
|
||||
}
|
||||
|
||||
/**
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
refresh()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "refresh() is called"
|
||||
def commands = []
|
||||
|
||||
@@ -37,7 +37,7 @@ metadata {
|
||||
attribute "minHeatingSetpoint", "number"
|
||||
attribute "maxCoolingSetpoint", "number"
|
||||
attribute "minCoolingSetpoint", "number"
|
||||
attribute "deviceTemperatureUnit", "number"
|
||||
attribute "deviceTemperatureUnit", "string"
|
||||
}
|
||||
|
||||
tiles {
|
||||
@@ -148,15 +148,13 @@ def generateEvent(Map results) {
|
||||
handlerName: name]
|
||||
|
||||
if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint" ) {
|
||||
def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F
|
||||
sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue
|
||||
def sendValue = location.temperatureScale == "C"? roundC(convertFtoC(value.toDouble())) : value.toInteger()
|
||||
isChange = isTemperatureStateChange(device, name, value.toString())
|
||||
isDisplayed = isChange
|
||||
event << [value: sendValue, isStateChange: isChange, displayed: isDisplayed]
|
||||
event << [value: sendValue, unit: temperatureScale, isStateChange: isChange, displayed: isDisplayed]
|
||||
} else if (name=="maxCoolingSetpoint" || name=="minCoolingSetpoint" || name=="maxHeatingSetpoint" || name=="minHeatingSetpoint") {
|
||||
def sendValue = convertTemperatureIfNeeded(value.toDouble(), "F", 1) //API return temperature value in F
|
||||
sendValue = location.temperatureScale == "C"? roundC(sendValue) : sendValue
|
||||
event << [value: sendValue, displayed: false]
|
||||
def sendValue = location.temperatureScale == "C"? roundC(convertFtoC(value.toDouble())) : value.toInteger()
|
||||
event << [value: sendValue, unit: temperatureScale, displayed: false]
|
||||
} else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){
|
||||
isChange = isStateChange(device, name, value.toString())
|
||||
event << [value: value.toString(), isStateChange: isChange, displayed: false]
|
||||
@@ -234,9 +232,9 @@ void setHeatingSetpoint(setpoint) {
|
||||
def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint
|
||||
|
||||
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
|
||||
if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) {
|
||||
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
|
||||
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
|
||||
if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) {
|
||||
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale)
|
||||
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint, "unit":location.temperatureScale)
|
||||
log.debug "Done setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}"
|
||||
generateSetpointEvent()
|
||||
generateStatusEvent()
|
||||
@@ -253,7 +251,6 @@ void setCoolingSetpoint(setpoint) {
|
||||
def maxCoolingSetpoint = device.currentValue("maxCoolingSetpoint")
|
||||
def minCoolingSetpoint = device.currentValue("minCoolingSetpoint")
|
||||
|
||||
|
||||
if (coolingSetpoint > maxCoolingSetpoint) {
|
||||
coolingSetpoint = maxCoolingSetpoint
|
||||
} else if (coolingSetpoint < minCoolingSetpoint) {
|
||||
@@ -271,9 +268,9 @@ void setCoolingSetpoint(setpoint) {
|
||||
def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint
|
||||
|
||||
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
|
||||
if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) {
|
||||
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
|
||||
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
|
||||
if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) {
|
||||
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale)
|
||||
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint, "unit":location.temperatureScale)
|
||||
log.debug "Done setCoolingSetpoint>> coolingSetpoint = ${coolingSetpoint}, heatingSetpoint = ${heatingSetpoint}"
|
||||
generateSetpointEvent()
|
||||
generateStatusEvent()
|
||||
@@ -283,18 +280,17 @@ void setCoolingSetpoint(setpoint) {
|
||||
}
|
||||
|
||||
void resumeProgram() {
|
||||
|
||||
log.debug "resumeProgram() is called"
|
||||
sendEvent("name":"thermostatStatus", "value":"resuming schedule", "description":statusText, displayed: false)
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
if (parent.resumeProgram(this, deviceId)) {
|
||||
if (parent.resumeProgram(deviceId)) {
|
||||
sendEvent("name":"thermostatStatus", "value":"setpoint is updating", "description":statusText, displayed: false)
|
||||
runIn(5, "poll")
|
||||
log.debug "resumeProgram() is done"
|
||||
sendEvent("name":"resumeProgram", "value":"resume", descriptionText: "resumeProgram is done", displayed: false, isStateChange: true)
|
||||
} else {
|
||||
sendEvent("name":"thermostatStatus", "value":"failed resume click refresh", "description":statusText, displayed: false)
|
||||
log.error "Error resumeProgram() check parent.resumeProgram(this, deviceId)"
|
||||
log.error "Error resumeProgram() check parent.resumeProgram(deviceId)"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -354,7 +350,6 @@ def switchFanMode() {
|
||||
}
|
||||
|
||||
def switchToFanMode(nextMode) {
|
||||
|
||||
log.debug "switching to fan mode: $nextMode"
|
||||
def returnCommand
|
||||
|
||||
@@ -406,7 +401,7 @@ def generateOperatingStateEvent(operatingState) {
|
||||
def off() {
|
||||
log.debug "off"
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
if (parent.setMode (this,"off", deviceId))
|
||||
if (parent.setMode ("off", deviceId))
|
||||
generateModeEvent("off")
|
||||
else {
|
||||
log.debug "Error setting new mode."
|
||||
@@ -420,7 +415,7 @@ def off() {
|
||||
def heat() {
|
||||
log.debug "heat"
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
if (parent.setMode (this,"heat", deviceId))
|
||||
if (parent.setMode ("heat", deviceId))
|
||||
generateModeEvent("heat")
|
||||
else {
|
||||
log.debug "Error setting new mode."
|
||||
@@ -438,7 +433,7 @@ def emergencyHeat() {
|
||||
def auxHeatOnly() {
|
||||
log.debug "auxHeatOnly"
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
if (parent.setMode (this,"auxHeatOnly", deviceId))
|
||||
if (parent.setMode ("auxHeatOnly", deviceId))
|
||||
generateModeEvent("auxHeatOnly")
|
||||
else {
|
||||
log.debug "Error setting new mode."
|
||||
@@ -452,7 +447,7 @@ def auxHeatOnly() {
|
||||
def cool() {
|
||||
log.debug "cool"
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
if (parent.setMode (this,"cool", deviceId))
|
||||
if (parent.setMode ("cool", deviceId))
|
||||
generateModeEvent("cool")
|
||||
else {
|
||||
log.debug "Error setting new mode."
|
||||
@@ -466,7 +461,7 @@ def cool() {
|
||||
def auto() {
|
||||
log.debug "auto"
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
if (parent.setMode (this,"auto", deviceId))
|
||||
if (parent.setMode ("auto", deviceId))
|
||||
generateModeEvent("auto")
|
||||
else {
|
||||
log.debug "Error setting new mode."
|
||||
@@ -489,7 +484,7 @@ def fanOn() {
|
||||
def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint
|
||||
def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint
|
||||
|
||||
if (parent.setFanMode(this, heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) {
|
||||
if (parent.setFanMode(heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) {
|
||||
generateFanModeEvent(fanMode)
|
||||
} else {
|
||||
log.debug "Error setting new mode."
|
||||
@@ -510,7 +505,7 @@ def fanAuto() {
|
||||
def coolingValue = location.temperatureScale == "C"? convertCtoF(coolingSetpoint) : coolingSetpoint
|
||||
def heatingValue = location.temperatureScale == "C"? convertCtoF(heatingSetpoint) : heatingSetpoint
|
||||
|
||||
if (parent.setFanMode(this, heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) {
|
||||
if (parent.setFanMode(heatingValue, coolingValue, deviceId, sendHoldType, fanMode)) {
|
||||
generateFanModeEvent(fanMode)
|
||||
} else {
|
||||
log.debug "Error setting new mode."
|
||||
@@ -520,63 +515,56 @@ def fanAuto() {
|
||||
}
|
||||
|
||||
def generateSetpointEvent() {
|
||||
|
||||
log.debug "Generate SetPoint Event"
|
||||
|
||||
def mode = device.currentValue("thermostatMode")
|
||||
log.debug "Current Mode = ${mode}"
|
||||
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint")
|
||||
log.debug "Heating Setpoint = ${heatingSetpoint}"
|
||||
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint")
|
||||
log.debug "Cooling Setpoint = ${coolingSetpoint}"
|
||||
|
||||
def maxHeatingSetpoint = device.currentValue("maxHeatingSetpoint")
|
||||
def maxCoolingSetpoint = device.currentValue("maxCoolingSetpoint")
|
||||
def minHeatingSetpoint = device.currentValue("minHeatingSetpoint")
|
||||
def minCoolingSetpoint = device.currentValue("minCoolingSetpoint")
|
||||
|
||||
if(location.temperatureScale == "C")
|
||||
{
|
||||
maxHeatingSetpoint = roundC(maxHeatingSetpoint)
|
||||
maxCoolingSetpoint = roundC(maxCoolingSetpoint)
|
||||
minHeatingSetpoint = roundC(minHeatingSetpoint)
|
||||
minCoolingSetpoint = roundC(minCoolingSetpoint)
|
||||
heatingSetpoint = roundC(heatingSetpoint)
|
||||
coolingSetpoint = roundC(coolingSetpoint)
|
||||
if(location.temperatureScale == "C") {
|
||||
maxHeatingSetpoint = maxHeatingSetpoint > 40 ? roundC(convertFtoC(maxHeatingSetpoint)) : roundC(maxHeatingSetpoint)
|
||||
maxCoolingSetpoint = maxCoolingSetpoint > 40 ? roundC(convertFtoC(maxCoolingSetpoint)) : roundC(maxCoolingSetpoint)
|
||||
minHeatingSetpoint = minHeatingSetpoint > 40 ? roundC(convertFtoC(minHeatingSetpoint)) : roundC(minHeatingSetpoint)
|
||||
minCoolingSetpoint = minCoolingSetpoint > 40 ? roundC(convertFtoC(minCoolingSetpoint)) : roundC(minCoolingSetpoint)
|
||||
heatingSetpoint = heatingSetpoint > 40 ? roundC(convertFtoC(heatingSetpoint)) : roundC(heatingSetpoint)
|
||||
coolingSetpoint = coolingSetpoint > 40 ? roundC(convertFtoC(coolingSetpoint)) : roundC(coolingSetpoint)
|
||||
} else {
|
||||
maxHeatingSetpoint = maxHeatingSetpoint < 40 ? roundC(convertCtoF(maxHeatingSetpoint)) : maxHeatingSetpoint
|
||||
maxCoolingSetpoint = maxCoolingSetpoint < 40 ? roundC(convertCtoF(maxCoolingSetpoint)) : maxCoolingSetpoint
|
||||
minHeatingSetpoint = minHeatingSetpoint < 40 ? roundC(convertCtoF(minHeatingSetpoint)) : minHeatingSetpoint
|
||||
minCoolingSetpoint = minCoolingSetpoint < 40 ? roundC(convertCtoF(minCoolingSetpoint)) : minCoolingSetpoint
|
||||
heatingSetpoint = heatingSetpoint < 40 ? roundC(convertCtoF(heatingSetpoint)) : heatingSetpoint
|
||||
coolingSetpoint = coolingSetpoint < 40 ? roundC(convertCtoF(coolingSetpoint)) : coolingSetpoint
|
||||
}
|
||||
|
||||
log.debug "Current Mode = ${mode}"
|
||||
log.debug "Heating Setpoint = ${heatingSetpoint}"
|
||||
log.debug "Cooling Setpoint = ${coolingSetpoint}"
|
||||
|
||||
sendEvent("name":"maxHeatingSetpoint", "value":maxHeatingSetpoint, "unit":location.temperatureScale)
|
||||
sendEvent("name":"maxCoolingSetpoint", "value":maxCoolingSetpoint, "unit":location.temperatureScale)
|
||||
sendEvent("name":"minHeatingSetpoint", "value":minHeatingSetpoint, "unit":location.temperatureScale)
|
||||
sendEvent("name":"minCoolingSetpoint", "value":minCoolingSetpoint, "unit":location.temperatureScale)
|
||||
|
||||
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale)
|
||||
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint, "unit":location.temperatureScale)
|
||||
|
||||
if (mode == "heat") {
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint )
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale)
|
||||
}
|
||||
else if (mode == "cool") {
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint)
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint, "unit":location.temperatureScale)
|
||||
} else if (mode == "auto") {
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":"Auto")
|
||||
|
||||
} else if (mode == "off") {
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":"Off")
|
||||
|
||||
} else if (mode == "auxHeatOnly") {
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint)
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint, "unit":location.temperatureScale)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void raiseSetpoint() {
|
||||
@@ -585,21 +573,31 @@ void raiseSetpoint() {
|
||||
def maxHeatingSetpoint = device.currentValue("maxHeatingSetpoint")
|
||||
def maxCoolingSetpoint = device.currentValue("maxCoolingSetpoint")
|
||||
|
||||
|
||||
if (mode == "off" || mode == "auto") {
|
||||
log.warn "this mode: $mode does not allow raiseSetpoint"
|
||||
} else {
|
||||
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint")
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint")
|
||||
def thermostatSetpoint = device.currentValue("thermostatSetpoint")
|
||||
|
||||
if (location.temperatureScale == "C") {
|
||||
maxHeatingSetpoint = maxHeatingSetpoint > 40 ? convertFtoC(maxHeatingSetpoint) : maxHeatingSetpoint
|
||||
maxCoolingSetpoint = maxCoolingSetpoint > 40 ? convertFtoC(maxCoolingSetpoint) : maxCoolingSetpoint
|
||||
heatingSetpoint = heatingSetpoint > 40 ? convertFtoC(heatingSetpoint) : heatingSetpoint
|
||||
coolingSetpoint = coolingSetpoint > 40 ? convertFtoC(coolingSetpoint) : coolingSetpoint
|
||||
thermostatSetpoint = thermostatSetpoint > 40 ? convertFtoC(thermostatSetpoint) : thermostatSetpoint
|
||||
} else {
|
||||
maxHeatingSetpoint = maxHeatingSetpoint < 40 ? convertCtoF(maxHeatingSetpoint) : maxHeatingSetpoint
|
||||
maxCoolingSetpoint = maxCoolingSetpoint < 40 ? convertCtoF(maxCoolingSetpoint) : maxCoolingSetpoint
|
||||
heatingSetpoint = heatingSetpoint < 40 ? convertCtoF(heatingSetpoint) : heatingSetpoint
|
||||
coolingSetpoint = coolingSetpoint < 40 ? convertCtoF(coolingSetpoint) : coolingSetpoint
|
||||
thermostatSetpoint = thermostatSetpoint < 40 ? convertCtoF(thermostatSetpoint) : thermostatSetpoint
|
||||
}
|
||||
|
||||
log.debug "raiseSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}"
|
||||
|
||||
if (device.latestState('thermostatSetpoint')) {
|
||||
targetvalue = device.latestState('thermostatSetpoint').value
|
||||
targetvalue = location.temperatureScale == "F"? targetvalue.toInteger() : targetvalue.toDouble()
|
||||
} else {
|
||||
targetvalue = 0
|
||||
}
|
||||
targetvalue = thermostatSetpoint ? thermostatSetpoint : 0
|
||||
targetvalue = location.temperatureScale == "F"? targetvalue + 1 : targetvalue + 0.5
|
||||
|
||||
if ((mode == "heat" || mode == "auxHeatOnly") && targetvalue > maxHeatingSetpoint) {
|
||||
@@ -608,7 +606,7 @@ void raiseSetpoint() {
|
||||
targetvalue = maxCoolingSetpoint
|
||||
}
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false)
|
||||
sendEvent("name":"thermostatSetpoint", "value":targetvalue, "unit":location.temperatureScale, displayed: false)
|
||||
log.info "In mode $mode raiseSetpoint() to $targetvalue"
|
||||
|
||||
runIn(3, "alterSetpoint", [data: [value:targetvalue], overwrite: true]) //when user click button this runIn will be overwrite
|
||||
@@ -622,20 +620,29 @@ void lowerSetpoint() {
|
||||
def minHeatingSetpoint = device.currentValue("minHeatingSetpoint")
|
||||
def minCoolingSetpoint = device.currentValue("minCoolingSetpoint")
|
||||
|
||||
|
||||
if (mode == "off" || mode == "auto") {
|
||||
log.warn "this mode: $mode does not allow lowerSetpoint"
|
||||
} else {
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint")
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint")
|
||||
def thermostatSetpoint = device.currentValue("thermostatSetpoint")
|
||||
log.debug "lowerSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}"
|
||||
if (device.latestState('thermostatSetpoint')) {
|
||||
targetvalue = device.latestState('thermostatSetpoint').value
|
||||
targetvalue = location.temperatureScale == "F"? targetvalue.toInteger() : targetvalue.toDouble()
|
||||
|
||||
if (location.temperatureScale == "C") {
|
||||
minHeatingSetpoint = minHeatingSetpoint > 40 ? convertFtoC(minHeatingSetpoint) : minHeatingSetpoint
|
||||
minCoolingSetpoint = minCoolingSetpoint > 40 ? convertFtoC(minCoolingSetpoint) : minCoolingSetpoint
|
||||
heatingSetpoint = heatingSetpoint > 40 ? convertFtoC(heatingSetpoint) : heatingSetpoint
|
||||
coolingSetpoint = coolingSetpoint > 40 ? convertFtoC(coolingSetpoint) : coolingSetpoint
|
||||
thermostatSetpoint = thermostatSetpoint > 40 ? convertFtoC(thermostatSetpoint) : thermostatSetpoint
|
||||
} else {
|
||||
targetvalue = 0
|
||||
minHeatingSetpoint = minHeatingSetpoint < 40 ? convertCtoF(minHeatingSetpoint) : minHeatingSetpoint
|
||||
minCoolingSetpoint = minCoolingSetpoint < 40 ? convertCtoF(minCoolingSetpoint) : minCoolingSetpoint
|
||||
heatingSetpoint = heatingSetpoint < 40 ? convertCtoF(heatingSetpoint) : heatingSetpoint
|
||||
coolingSetpoint = coolingSetpoint < 40 ? convertCtoF(coolingSetpoint) : coolingSetpoint
|
||||
thermostatSetpoint = thermostatSetpoint < 40 ? convertCtoF(thermostatSetpoint) : thermostatSetpoint
|
||||
}
|
||||
log.debug "lowerSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}"
|
||||
|
||||
targetvalue = thermostatSetpoint ? thermostatSetpoint : 0
|
||||
targetvalue = location.temperatureScale == "F"? targetvalue - 1 : targetvalue - 0.5
|
||||
|
||||
if ((mode == "heat" || mode == "auxHeatOnly") && targetvalue < minHeatingSetpoint) {
|
||||
@@ -644,7 +651,7 @@ void lowerSetpoint() {
|
||||
targetvalue = minCoolingSetpoint
|
||||
}
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":targetvalue, displayed: false)
|
||||
sendEvent("name":"thermostatSetpoint", "value":targetvalue, "unit":location.temperatureScale, displayed: false)
|
||||
log.info "In mode $mode lowerSetpoint() to $targetvalue"
|
||||
|
||||
runIn(3, "alterSetpoint", [data: [value:targetvalue], overwrite: true]) //when user click button this runIn will be overwrite
|
||||
@@ -653,8 +660,11 @@ void lowerSetpoint() {
|
||||
|
||||
//called by raiseSetpoint() and lowerSetpoint()
|
||||
void alterSetpoint(temp) {
|
||||
|
||||
def mode = device.currentValue("thermostatMode")
|
||||
|
||||
if (mode == "off" || mode == "auto") {
|
||||
log.warn "this mode: $mode does not allow alterSetpoint"
|
||||
} else {
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint")
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint")
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
@@ -662,6 +672,18 @@ void alterSetpoint(temp) {
|
||||
def targetHeatingSetpoint
|
||||
def targetCoolingSetpoint
|
||||
|
||||
def temperatureScaleHasChanged = false
|
||||
|
||||
if (location.temperatureScale == "C") {
|
||||
if ( heatingSetpoint > 40.0 || coolingSetpoint > 40.0 ) {
|
||||
temperatureScaleHasChanged = true
|
||||
}
|
||||
} else {
|
||||
if ( heatingSetpoint < 40.0 || coolingSetpoint < 40.0 ) {
|
||||
temperatureScaleHasChanged = true
|
||||
}
|
||||
}
|
||||
|
||||
//step1: check thermostatMode, enforce limits before sending request to cloud
|
||||
if (mode == "heat" || mode == "auxHeatOnly"){
|
||||
if (temp.value > coolingSetpoint){
|
||||
@@ -690,10 +712,10 @@ void alterSetpoint(temp) {
|
||||
def coolingValue = location.temperatureScale == "C"? convertCtoF(targetCoolingSetpoint) : targetCoolingSetpoint
|
||||
def heatingValue = location.temperatureScale == "C"? convertCtoF(targetHeatingSetpoint) : targetHeatingSetpoint
|
||||
|
||||
if (parent.setHold(this, heatingValue, coolingValue, deviceId, sendHoldType)) {
|
||||
if (parent.setHold(heatingValue, coolingValue, deviceId, sendHoldType)) {
|
||||
sendEvent("name": "thermostatSetpoint", "value": temp.value, displayed: false)
|
||||
sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint)
|
||||
sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint)
|
||||
sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint, "unit": location.temperatureScale)
|
||||
sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint, "unit": location.temperatureScale)
|
||||
log.debug "alterSetpoint in mode $mode succeed change setpoint to= ${temp.value}"
|
||||
} else {
|
||||
log.error "Error alterSetpoint()"
|
||||
@@ -703,16 +725,18 @@ void alterSetpoint(temp) {
|
||||
sendEvent("name": "thermostatSetpoint", "value": coolingSetpoint.toString(), displayed: false)
|
||||
}
|
||||
}
|
||||
|
||||
if ( temperatureScaleHasChanged )
|
||||
generateSetpointEvent()
|
||||
generateStatusEvent()
|
||||
}
|
||||
}
|
||||
|
||||
def generateStatusEvent() {
|
||||
|
||||
def mode = device.currentValue("thermostatMode")
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint")
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint")
|
||||
def temperature = device.currentValue("temperature")
|
||||
|
||||
def statusText
|
||||
|
||||
log.debug "Generate Status Event for Mode = ${mode}"
|
||||
@@ -722,36 +746,25 @@ def generateStatusEvent() {
|
||||
log.debug "HVAC Mode = ${mode}"
|
||||
|
||||
if (mode == "heat") {
|
||||
|
||||
if (temperature >= heatingSetpoint)
|
||||
statusText = "Right Now: Idle"
|
||||
else
|
||||
statusText = "Heating to ${heatingSetpoint} ${location.temperatureScale}"
|
||||
|
||||
} else if (mode == "cool") {
|
||||
|
||||
if (temperature <= coolingSetpoint)
|
||||
statusText = "Right Now: Idle"
|
||||
else
|
||||
statusText = "Cooling to ${coolingSetpoint} ${location.temperatureScale}"
|
||||
|
||||
} else if (mode == "auto") {
|
||||
|
||||
statusText = "Right Now: Auto"
|
||||
|
||||
} else if (mode == "off") {
|
||||
|
||||
statusText = "Right Now: Off"
|
||||
|
||||
} else if (mode == "auxHeatOnly") {
|
||||
|
||||
statusText = "Emergency Heat"
|
||||
|
||||
} else {
|
||||
|
||||
statusText = "?"
|
||||
|
||||
}
|
||||
|
||||
log.debug "Generate Status Event = ${statusText}"
|
||||
sendEvent("name":"thermostatStatus", "value":statusText, "description":statusText, displayed: true)
|
||||
}
|
||||
@@ -765,7 +778,7 @@ def roundC (tempC) {
|
||||
}
|
||||
|
||||
def convertFtoC (tempF) {
|
||||
return String.format("%.1f", (Math.round(((tempF - 32)*(5/9)) * 2))/2)
|
||||
return ((Math.round(((tempF - 32)*(5/9)) * 2))/2).toDouble()
|
||||
}
|
||||
|
||||
def convertCtoF (tempC) {
|
||||
|
||||
@@ -28,17 +28,6 @@ metadata {
|
||||
|
||||
}
|
||||
|
||||
// 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") {
|
||||
@@ -50,18 +39,15 @@ metadata {
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel"
|
||||
}
|
||||
tileAttribute ("power", key: "SECONDARY_CONTROL") {
|
||||
attributeState "power", label:'${currentValue} W'
|
||||
}
|
||||
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "level", label: 'Level ${currentValue}%'
|
||||
}
|
||||
valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) {
|
||||
state "power", label:'${currentValue} W'
|
||||
}
|
||||
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
main "switch"
|
||||
details(["switch", "level", "power","levelSliderControl","refresh"])
|
||||
details(["switch", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,283 +55,42 @@ metadata {
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def finalResult = isKnownDescription(description)
|
||||
if (finalResult != "false") {
|
||||
log.info finalResult
|
||||
if (finalResult.type == "update") {
|
||||
log.info "$device updates: ${finalResult.value}"
|
||||
def event = zigbee.getEvent(description)
|
||||
if (event) {
|
||||
log.info event
|
||||
if (event.name == "power") {
|
||||
if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power
|
||||
event.value = (event.value as Integer) / 10 //TODO: The divisor value needs to be set as part of configuration
|
||||
sendEvent(event)
|
||||
}
|
||||
else if (finalResult.type == "power") {
|
||||
def powerValue = (finalResult.value as Integer)/10
|
||||
sendEvent(name: "power", value: powerValue)
|
||||
|
||||
/*
|
||||
Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10
|
||||
|
||||
power level is an integer. The exact power level with correct units needs to be handled in the device type
|
||||
to account for the different Divisor value (AttrId: 0302) and POWER Unit (AttrId: 0300). CLUSTER for simple metering is 0702
|
||||
*/
|
||||
}
|
||||
else {
|
||||
sendEvent(name: finalResult.type, value: finalResult.value)
|
||||
sendEvent(event)
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug parseDescriptionAsMap(description)
|
||||
log.debug zigbee.parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
// Commands to device
|
||||
def zigbeeCommand(cluster, attribute){
|
||||
["st cmd 0x${device.deviceNetworkId} ${endpointId} ${cluster} ${attribute} {}"]
|
||||
}
|
||||
|
||||
def off() {
|
||||
zigbeeCommand("6", "0")
|
||||
zigbee.off()
|
||||
}
|
||||
|
||||
def on() {
|
||||
zigbeeCommand("6", "1")
|
||||
zigbee.on()
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
value = value as Integer
|
||||
if (value == 0) {
|
||||
off()
|
||||
}
|
||||
else {
|
||||
sendEvent(name: "level", value: value)
|
||||
setLevelWithRate(value, "0000") + ["delay 1000"] + on() //value is between 0 to 100; GE does NOT switch on if OFF
|
||||
}
|
||||
zigbee.setLevel(value)
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0702 0x0400", "delay 500"
|
||||
]
|
||||
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
onOffConfig() + levelConfig() + powerConfig() + refresh()
|
||||
}
|
||||
|
||||
|
||||
private getEndpointId() {
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
|
||||
private hex(value, width=2) {
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() < width) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
//Need to reverse array of size 2
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
byte tmp;
|
||||
tmp = array[1];
|
||||
array[1] = array[0];
|
||||
array[0] = tmp;
|
||||
return array
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
if (description?.startsWith("read attr -")) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()): nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
else if (description?.startsWith("catchall: ")) {
|
||||
def seg = (description - "catchall: ").split(" ")
|
||||
def zigbeeMap = [:]
|
||||
zigbeeMap += [raw: (description - "catchall: ")]
|
||||
zigbeeMap += [profileId: seg[0]]
|
||||
zigbeeMap += [clusterId: seg[1]]
|
||||
zigbeeMap += [sourceEndpoint: seg[2]]
|
||||
zigbeeMap += [destinationEndpoint: seg[3]]
|
||||
zigbeeMap += [options: seg[4]]
|
||||
zigbeeMap += [messageType: seg[5]]
|
||||
zigbeeMap += [dni: seg[6]]
|
||||
zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0]
|
||||
zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0]
|
||||
zigbeeMap += [manufacturerId: seg[9]]
|
||||
zigbeeMap += [command: seg[10]]
|
||||
zigbeeMap += [direction: seg[11]]
|
||||
zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect {
|
||||
it.join('')
|
||||
} : []]
|
||||
|
||||
zigbeeMap
|
||||
}
|
||||
}
|
||||
|
||||
def isKnownDescription(description) {
|
||||
if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
if (descMap.cluster == "0006" || descMap.clusterId == "0006") {
|
||||
isDescriptionOnOff(descMap)
|
||||
}
|
||||
else if (descMap.cluster == "0008" || descMap.clusterId == "0008"){
|
||||
isDescriptionLevel(descMap)
|
||||
}
|
||||
else if (descMap.cluster == "0702" || descMap.clusterId == "0702"){
|
||||
isDescriptionPower(descMap)
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
else if(description?.startsWith("on/off:")) {
|
||||
def switchValue = description?.endsWith("1") ? "on" : "off"
|
||||
return [type: "switch", value : switchValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
def isDescriptionOnOff(descMap) {
|
||||
def switchValue = "undefined"
|
||||
if (descMap.cluster == "0006") { //cluster info from read attr
|
||||
value = descMap.value
|
||||
if (value == "01"){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if (value == "00"){
|
||||
switchValue = "off"
|
||||
}
|
||||
}
|
||||
else if (descMap.clusterId == "0006") {
|
||||
//cluster info from catch all
|
||||
//command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00
|
||||
//command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00
|
||||
if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){
|
||||
switchValue = "off"
|
||||
}
|
||||
else if(descMap.command=="07"){
|
||||
return [type: "update", value : "switch (0006) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (switchValue != "undefined"){
|
||||
return [type: "switch", value : switchValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//@return - false or "success" or level [0-100]
|
||||
def isDescriptionLevel(descMap) {
|
||||
def dimmerValue = -1
|
||||
if (descMap.cluster == "0008"){
|
||||
//TODO: the message returned with catchall is command 0B with clusterId 0008. That is just a confirmation message
|
||||
def value = convertHexToInt(descMap.value)
|
||||
dimmerValue = Math.round(value * 100 / 255)
|
||||
if(dimmerValue==0 && value > 0) {
|
||||
dimmerValue = 1 //handling for non-zero hex value less than 3
|
||||
}
|
||||
}
|
||||
else if(descMap.clusterId == "0008") {
|
||||
if(descMap.command=="0B"){
|
||||
return [type: "update", value : "level updated successfully"] //device updating the level change was successful. no value sent.
|
||||
}
|
||||
else if(descMap.command=="07"){
|
||||
return [type: "update", value : "level (0008) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (dimmerValue != -1){
|
||||
return [type: "level", value : dimmerValue]
|
||||
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
def isDescriptionPower(descMap) {
|
||||
def powerValue = "undefined"
|
||||
if (descMap.cluster == "0702") {
|
||||
if (descMap.attrId == "0400") {
|
||||
powerValue = convertHexToInt(descMap.value)
|
||||
}
|
||||
}
|
||||
else if (descMap.clusterId == "0702") {
|
||||
if(descMap.command=="07"){
|
||||
return [type: "update", value : "power (0702) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (powerValue != "undefined"){
|
||||
return [type: "power", value : powerValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def onOffConfig() {
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 6 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 6 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||
]
|
||||
}
|
||||
|
||||
//level config for devices with min reporting interval as 5 seconds and reporting interval if no activity as 1hour (3600s)
|
||||
//min level change is 01
|
||||
def levelConfig() {
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 8 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 8 0 0x20 1 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||
]
|
||||
}
|
||||
|
||||
//power config for devices with min reporting interval as 1 seconds and reporting interval if no activity as 10min (600s)
|
||||
//min change in value is 05
|
||||
def powerConfig() {
|
||||
[
|
||||
//Meter (Power) Reporting
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x0702 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0702 0x0400 0x2A 1 600 {05}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||
]
|
||||
}
|
||||
|
||||
def setLevelWithRate(level, rate) {
|
||||
if(rate == null){
|
||||
rate = "0000"
|
||||
}
|
||||
level = convertToHexString(level * 255 / 100) //Converting the 0-100 range to 0-FF range in hex
|
||||
["st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {$level $rate}"]
|
||||
}
|
||||
|
||||
String convertToHexString(value, width=2) {
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() < width) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
refresh()
|
||||
}
|
||||
|
||||
@@ -28,17 +28,6 @@ metadata {
|
||||
|
||||
}
|
||||
|
||||
// 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") {
|
||||
@@ -47,238 +36,51 @@ metadata {
|
||||
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) {
|
||||
}
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
valueTile("power", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "power", label:'${currentValue} Watts'
|
||||
}
|
||||
main "switch"
|
||||
details(["switch", "power", "refresh"])
|
||||
details(["switch", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def finalResult = isKnownDescription(description)
|
||||
if (finalResult != "false") {
|
||||
log.info finalResult
|
||||
if (finalResult.type == "update") {
|
||||
log.info "$device updates: ${finalResult.value}"
|
||||
}
|
||||
else if (finalResult.type == "power") {
|
||||
def powerValue = (finalResult.value as Integer)/10
|
||||
def event = zigbee.getEvent(description)
|
||||
if (event) {
|
||||
if (event.name == "power") {
|
||||
def powerValue
|
||||
powerValue = (event.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration
|
||||
sendEvent(name: "power", value: powerValue)
|
||||
|
||||
/*
|
||||
Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10
|
||||
|
||||
power level is an integer. The exact power level with correct units needs to be handled in the device type
|
||||
to account for the different Divisor value (AttrId: 0302) and POWER Unit (AttrId: 0300). CLUSTER for simple metering is 0702
|
||||
*/
|
||||
}
|
||||
else {
|
||||
sendEvent(name: finalResult.type, value: finalResult.value)
|
||||
sendEvent(event)
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug parseDescriptionAsMap(description)
|
||||
log.debug zigbee.parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
// Commands to device
|
||||
def zigbeeCommand(cluster, attribute){
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} ${cluster} ${attribute} {}"
|
||||
}
|
||||
|
||||
def off() {
|
||||
zigbeeCommand("6", "0")
|
||||
zigbee.off()
|
||||
}
|
||||
|
||||
def on() {
|
||||
zigbeeCommand("6", "1")
|
||||
zigbee.on()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0702 0x0400", "delay 500"
|
||||
]
|
||||
|
||||
zigbee.onOffRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
onOffConfig() + powerConfig() + refresh()
|
||||
}
|
||||
|
||||
|
||||
private getEndpointId() {
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
|
||||
private hex(value, width=2) {
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() < width) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
//Need to reverse array of size 2
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
byte tmp;
|
||||
tmp = array[1];
|
||||
array[1] = array[0];
|
||||
array[0] = tmp;
|
||||
return array
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
if (description?.startsWith("read attr -")) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()): nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
else if (description?.startsWith("catchall: ")) {
|
||||
def seg = (description - "catchall: ").split(" ")
|
||||
def zigbeeMap = [:]
|
||||
zigbeeMap += [raw: (description - "catchall: ")]
|
||||
zigbeeMap += [profileId: seg[0]]
|
||||
zigbeeMap += [clusterId: seg[1]]
|
||||
zigbeeMap += [sourceEndpoint: seg[2]]
|
||||
zigbeeMap += [destinationEndpoint: seg[3]]
|
||||
zigbeeMap += [options: seg[4]]
|
||||
zigbeeMap += [messageType: seg[5]]
|
||||
zigbeeMap += [dni: seg[6]]
|
||||
zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0]
|
||||
zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0]
|
||||
zigbeeMap += [manufacturerId: seg[9]]
|
||||
zigbeeMap += [command: seg[10]]
|
||||
zigbeeMap += [direction: seg[11]]
|
||||
zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect {
|
||||
it.join('')
|
||||
} : []]
|
||||
|
||||
zigbeeMap
|
||||
}
|
||||
}
|
||||
|
||||
def isKnownDescription(description) {
|
||||
if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
if (descMap.cluster == "0006" || descMap.clusterId == "0006") {
|
||||
isDescriptionOnOff(descMap)
|
||||
}
|
||||
else if (descMap.cluster == "0702" || descMap.clusterId == "0702"){
|
||||
isDescriptionPower(descMap)
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
else if(description?.startsWith("on/off:")) {
|
||||
def switchValue = description?.endsWith("1") ? "on" : "off"
|
||||
return [type: "switch", value : switchValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
def isDescriptionOnOff(descMap) {
|
||||
def switchValue = "undefined"
|
||||
if (descMap.cluster == "0006") { //cluster info from read attr
|
||||
value = descMap.value
|
||||
if (value == "01"){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if (value == "00"){
|
||||
switchValue = "off"
|
||||
}
|
||||
}
|
||||
else if (descMap.clusterId == "0006") {
|
||||
//cluster info from catch all
|
||||
//command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00
|
||||
//command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00
|
||||
if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){
|
||||
switchValue = "on"
|
||||
}
|
||||
else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){
|
||||
switchValue = "off"
|
||||
}
|
||||
else if(descMap.command=="07"){
|
||||
return [type: "update", value : "switch (0006) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (switchValue != "undefined"){
|
||||
return [type: "switch", value : switchValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def isDescriptionPower(descMap) {
|
||||
def powerValue = "undefined"
|
||||
if (descMap.cluster == "0702") {
|
||||
if (descMap.attrId == "0400") {
|
||||
powerValue = convertHexToInt(descMap.value)
|
||||
}
|
||||
}
|
||||
else if (descMap.clusterId == "0702") {
|
||||
if(descMap.command=="07"){
|
||||
return [type: "update", value : "power (0702) capability configured successfully"]
|
||||
}
|
||||
}
|
||||
|
||||
if (powerValue != "undefined"){
|
||||
return [type: "power", value : powerValue]
|
||||
}
|
||||
else {
|
||||
return "false"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def onOffConfig() {
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 6 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 6 0 0x10 0 600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||
]
|
||||
}
|
||||
|
||||
//power config for devices with min reporting interval as 1 seconds and reporting interval if no activity as 10min (600s)
|
||||
//min change in value is 05
|
||||
def powerConfig() {
|
||||
[
|
||||
//Meter (Power) Reporting
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x0702 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x0702 0x0400 0x2A 1 600 {05}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||
]
|
||||
}
|
||||
|
||||
String convertToHexString(value, width=2) {
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() < width) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
refresh()
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ metadata {
|
||||
capability "Switch"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Health Check"
|
||||
|
||||
command "setAdjustedColor"
|
||||
command "reset"
|
||||
@@ -55,6 +56,10 @@ metadata {
|
||||
}
|
||||
}
|
||||
|
||||
void installed() {
|
||||
sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device.hub.hardwareID}\"}")
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(description) {
|
||||
log.debug "parse() - $description"
|
||||
@@ -166,3 +171,4 @@ def verifyPercent(percent) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
metadata {
|
||||
// Automatically generated. Make future change here.
|
||||
definition (name: "Hue Bridge", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Health Check"
|
||||
|
||||
attribute "networkAddress", "string"
|
||||
// Used to indicate if bridge is reachable or not, i.e. is the bridge connected to the network
|
||||
// Possible values "Online" or "Offline"
|
||||
@@ -42,6 +44,10 @@ metadata {
|
||||
}
|
||||
}
|
||||
|
||||
void installed() {
|
||||
sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device.hub.hardwareID}\"}")
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
@@ -70,13 +76,8 @@ def parse(description) {
|
||||
def bulbs = new groovy.json.JsonSlurper().parseText(msg.body)
|
||||
if (bulbs.state) {
|
||||
log.info "Bridge response: $msg.body"
|
||||
} else {
|
||||
// Sending Bulbs List to parent"
|
||||
if (parent.isInBulbDiscovery())
|
||||
log.info parent.bulbListHandler(device.hub.id, msg.body)
|
||||
}
|
||||
}
|
||||
else if (contentType?.contains("xml")) {
|
||||
} else if (contentType?.contains("xml")) {
|
||||
log.debug "HUE BRIDGE ALREADY PRESENT"
|
||||
parent.hubVerification(device.hub.id, msg.body)
|
||||
}
|
||||
@@ -85,3 +86,4 @@ def parse(description) {
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ metadata {
|
||||
capability "Switch"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Health Check"
|
||||
|
||||
command "setAdjustedColor"
|
||||
command "reset"
|
||||
@@ -64,6 +65,10 @@ metadata {
|
||||
}
|
||||
}
|
||||
|
||||
void installed() {
|
||||
sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device.hub.hardwareID}\"}")
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(description) {
|
||||
log.debug "parse() - $description"
|
||||
@@ -169,7 +174,7 @@ void setColorTemperature(value) {
|
||||
|
||||
void refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
parent.manualRefresh()
|
||||
parent?.manualRefresh()
|
||||
}
|
||||
|
||||
def verifyPercent(percent) {
|
||||
@@ -182,3 +187,4 @@ def verifyPercent(percent) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ metadata {
|
||||
capability "Switch"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Health Check"
|
||||
|
||||
command "refresh"
|
||||
}
|
||||
@@ -48,6 +49,10 @@ metadata {
|
||||
}
|
||||
}
|
||||
|
||||
void installed() {
|
||||
sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device.hub.hardwareID}\"}")
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(description) {
|
||||
log.debug "parse() - $description"
|
||||
@@ -87,3 +92,4 @@ void refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
parent.manualRefresh()
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ metadata {
|
||||
capability "Color Temperature"
|
||||
capability "Switch"
|
||||
capability "Refresh"
|
||||
capability "Health Check"
|
||||
|
||||
command "refresh"
|
||||
}
|
||||
@@ -53,6 +54,10 @@ metadata {
|
||||
}
|
||||
}
|
||||
|
||||
void installed() {
|
||||
sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device.hub.hardwareID}\"}")
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(description) {
|
||||
log.debug "parse() - $description"
|
||||
@@ -101,3 +106,4 @@ void refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
parent.manualRefresh()
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ metadata {
|
||||
capability "Color Temperature"
|
||||
capability "Switch"
|
||||
capability "Switch Level" // brightness
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Health Check"
|
||||
}
|
||||
|
||||
simulator {
|
||||
@@ -23,7 +23,6 @@ metadata {
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "unreachable", label: "?", action:"refresh.refresh", icon:"http://hosted.lifx.co/smartthings/v1/196xUnreachable.png", backgroundColor:"#666666"
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'Turning on', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
@@ -64,12 +63,8 @@ metadata {
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
if (description == 'updated') {
|
||||
return // don't poll when config settings is being updated as it may time out
|
||||
}
|
||||
poll()
|
||||
void installed() {
|
||||
sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"cloud\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device?.hub?.hardwareID}\"}")
|
||||
}
|
||||
|
||||
// handle commands
|
||||
@@ -141,7 +136,6 @@ def setLevel(percentage) {
|
||||
percentage = 1 // clamp to 1%
|
||||
}
|
||||
if (percentage == 0) {
|
||||
sendEvent(name: "level", value: 0) // Otherwise the level value tile does not update
|
||||
return off() // if the brightness is set to 0, just turn it off
|
||||
}
|
||||
parent.logErrors(logObject:log) {
|
||||
@@ -193,14 +187,17 @@ def off() {
|
||||
return []
|
||||
}
|
||||
|
||||
def poll() {
|
||||
log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}"
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
|
||||
def resp = parent.apiGET("/lights/${selector()}")
|
||||
if (resp.status == 404) {
|
||||
sendEvent(name: "switch", value: "unreachable")
|
||||
state.online = false
|
||||
sendEvent(name: "DeviceWatch-DeviceStatusUpdate", value: "offline", displayed: false)
|
||||
log.warn "$device is Offline"
|
||||
return []
|
||||
} else if (resp.status != 200) {
|
||||
log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}")
|
||||
log.error("Unexpected result in refresh(): [${resp.status}] ${resp.data}")
|
||||
return []
|
||||
}
|
||||
def data = resp.data[0]
|
||||
@@ -209,19 +206,20 @@ def poll() {
|
||||
sendEvent(name: "label", value: data.label)
|
||||
sendEvent(name: "level", value: Math.round((data.brightness ?: 1) * 100))
|
||||
sendEvent(name: "switch.setLevel", value: Math.round((data.brightness ?: 1) * 100))
|
||||
sendEvent(name: "switch", value: data.connected ? data.power : "unreachable")
|
||||
sendEvent(name: "switch", value: data.power)
|
||||
sendEvent(name: "color", value: colorUtil.hslToHex((data.color.hue / 3.6) as int, (data.color.saturation * 100) as int))
|
||||
sendEvent(name: "hue", value: data.color.hue / 3.6)
|
||||
sendEvent(name: "saturation", value: data.color.saturation * 100)
|
||||
sendEvent(name: "colorTemperature", value: data.color.kelvin)
|
||||
sendEvent(name: "model", value: "${data.product.company} ${data.product.name}")
|
||||
sendEvent(name: "model", value: data.product.name)
|
||||
|
||||
return []
|
||||
if (data.connected) {
|
||||
sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false)
|
||||
log.debug "$device is Online"
|
||||
} else {
|
||||
sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false)
|
||||
log.warn "$device is Offline"
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
poll()
|
||||
}
|
||||
|
||||
def selector() {
|
||||
|
||||
@@ -10,9 +10,9 @@ metadata {
|
||||
capability "Color Temperature"
|
||||
capability "Switch"
|
||||
capability "Switch Level" // brightness
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Health Check"
|
||||
}
|
||||
|
||||
simulator {
|
||||
@@ -22,13 +22,12 @@ metadata {
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "unreachable", label: "?", action:"refresh.refresh", icon:"http://hosted.lifx.co/smartthings/v1/196xUnreachable.png", backgroundColor:"#666666"
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'Turning on', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'Turning off', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
|
||||
}
|
||||
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel"
|
||||
}
|
||||
@@ -53,15 +52,10 @@ metadata {
|
||||
main "switch"
|
||||
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
if (description == 'updated') {
|
||||
return // don't poll when config settings is being updated as it may time out
|
||||
}
|
||||
poll()
|
||||
void installed() {
|
||||
sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"cloud\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device?.hub?.hardwareID}\"}")
|
||||
}
|
||||
|
||||
// handle commands
|
||||
@@ -71,7 +65,6 @@ def setLevel(percentage) {
|
||||
percentage = 1 // clamp to 1%
|
||||
}
|
||||
if (percentage == 0) {
|
||||
sendEvent(name: "level", value: 0) // Otherwise the level value tile does not update
|
||||
return off() // if the brightness is set to 0, just turn it off
|
||||
}
|
||||
parent.logErrors(logObject:log) {
|
||||
@@ -123,14 +116,17 @@ def off() {
|
||||
return []
|
||||
}
|
||||
|
||||
def poll() {
|
||||
log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}"
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
|
||||
def resp = parent.apiGET("/lights/${selector()}")
|
||||
if (resp.status == 404) {
|
||||
sendEvent(name: "switch", value: "unreachable")
|
||||
state.online = false
|
||||
sendEvent(name: "DeviceWatch-DeviceStatusUpdate", value: "offline", displayed: false)
|
||||
log.warn "$device is Offline"
|
||||
return []
|
||||
} else if (resp.status != 200) {
|
||||
log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}")
|
||||
log.error("Unexpected result in refresh(): [${resp.status}] ${resp.data}")
|
||||
return []
|
||||
}
|
||||
def data = resp.data[0]
|
||||
@@ -138,16 +134,17 @@ def poll() {
|
||||
sendEvent(name: "label", value: data.label)
|
||||
sendEvent(name: "level", value: Math.round((data.brightness ?: 1) * 100))
|
||||
sendEvent(name: "switch.setLevel", value: Math.round((data.brightness ?: 1) * 100))
|
||||
sendEvent(name: "switch", value: data.connected ? data.power : "unreachable")
|
||||
sendEvent(name: "switch", value: data.power)
|
||||
sendEvent(name: "colorTemperature", value: data.color.kelvin)
|
||||
sendEvent(name: "model", value: data.product.name)
|
||||
|
||||
return []
|
||||
if (data.connected) {
|
||||
sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false)
|
||||
log.debug "$device is Online"
|
||||
} else {
|
||||
sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false)
|
||||
log.warn "$device is Offline"
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
poll()
|
||||
}
|
||||
|
||||
def selector() {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.st-ignore
|
||||
README.md
|
||||
@@ -0,0 +1,37 @@
|
||||
# Nyce Door/Window Sensor (Open/Close Sensor)
|
||||
|
||||
|
||||
|
||||
Works with:
|
||||
|
||||
* [NYCE Door/Window Sensor NCZ-3011](https://support.smartthings.com/hc/en-us/articles/204576764-NYCE-Door-Window-Sensor)
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Capabilities](#capabilities)
|
||||
* [Health](#device-health)
|
||||
* [Battery](#battery-specification)
|
||||
* [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Capabilities
|
||||
|
||||
* **Configuration** - _configure()_ command called when device is installed or device preferences updated
|
||||
* **Contact Sensor** - can detect contact (with possible values - open/closed)
|
||||
* **Battery** - defines device uses a battery
|
||||
* **Refresh** - _refresh()_ command for status updates
|
||||
* **Health Check** - indicates ability to get device health notifications
|
||||
|
||||
## Device Health
|
||||
|
||||
A Category C2 Nyce Door/Window sensor that has 12min check-in interval
|
||||
|
||||
## Battery Specification
|
||||
|
||||
One 3V CR2032 battery required.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the sensor is out of range.
|
||||
Pairing needs to be tried again by placing the sensor closer to the hub.
|
||||
Instructions related to pairing, resetting and removing the sensor from SmartThings can be found in the following link:
|
||||
* [Nyce Door/Window Sensor](https://support.smartthings.com/hc/en-us/articles/204576764-NYCE-Door-Window-Sensor)
|
||||
@@ -23,6 +23,7 @@ metadata {
|
||||
capability "Configuration"
|
||||
capability "Contact Sensor"
|
||||
capability "Refresh"
|
||||
capability "Health Check"
|
||||
|
||||
command "enrollResponse"
|
||||
|
||||
@@ -273,23 +274,28 @@ private List parseIasMessage(String description) {
|
||||
return resultListMap
|
||||
}
|
||||
|
||||
/**
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
||||
}
|
||||
|
||||
def configure() {
|
||||
// Device-Watch allows 2 check-in misses from device
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
|
||||
def configCmds = [
|
||||
//battery reporting and heartbeat
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500",
|
||||
|
||||
|
||||
def enrollCmds = [
|
||||
// Writes CIE attribute on end device to direct reports to the hub's EUID
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
]
|
||||
|
||||
log.debug "configure: Write IAS CIE"
|
||||
return configCmds
|
||||
// battery minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
return enrollCmds + zigbee.batteryConfig(30, 300) + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
@@ -334,7 +340,8 @@ Integer convertHexToInt(hex) {
|
||||
|
||||
def refresh() {
|
||||
log.debug "Refreshing Battery"
|
||||
[
|
||||
def refreshCmds = [
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 1 0x20", "delay 200"
|
||||
]
|
||||
return refreshCmds + enrollResponse()
|
||||
}
|
||||
|
||||
@@ -21,364 +21,125 @@ metadata {
|
||||
attribute "colorName", "string"
|
||||
|
||||
command "setAdjustedColor"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenspot RGB"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Gardenspot RGB"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
// 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", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821"
|
||||
state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff"
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel"
|
||||
}
|
||||
tileAttribute ("device.color", key: "COLOR_CONTROL") {
|
||||
attributeState "color", action:"color control.setColor"
|
||||
}
|
||||
}
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) {
|
||||
state "color", action:"setAdjustedColor"
|
||||
}
|
||||
valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat") {
|
||||
state "colorName", label: '${currentValue}'
|
||||
}
|
||||
|
||||
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", "refresh", "colorName", "levelSliderControl", "level", "rgbSelector"])
|
||||
details(["switch", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
//Globals
|
||||
private getATTRIBUTE_HUE() { 0x0000 }
|
||||
private getATTRIBUTE_SATURATION() { 0x0001 }
|
||||
private getHUE_COMMAND() { 0x00 }
|
||||
private getSATURATION_COMMAND() { 0x03 }
|
||||
private getCOLOR_CONTROL_CLUSTER() { 0x0300 }
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
//log.info "description is $description"
|
||||
if (description?.startsWith("catchall:")) {
|
||||
if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1"))
|
||||
{
|
||||
def result = createEvent(name: "switch", value: "on")
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
}
|
||||
else if(description?.endsWith("0000") || description?.endsWith("1000") || description?.matches("on/off\\s*:\\s*0"))
|
||||
{
|
||||
if(!(description?.startsWith("catchall: 0104 0300"))){
|
||||
def result = createEvent(name: "switch", value: "off")
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (description?.startsWith("read attr -")) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
log.trace "descMap : $descMap"
|
||||
log.debug "description is $description"
|
||||
|
||||
if (descMap.cluster == "0300") {
|
||||
if(descMap.attrId == "0000"){ //Hue Attribute
|
||||
def hueValue = Math.round(convertHexToInt(descMap.value) / 255 * 360)
|
||||
log.debug "Hue value returned is $hueValue"
|
||||
sendEvent(name: "hue", value: hueValue, displayed:false)
|
||||
}
|
||||
else if(descMap.attrId == "0001"){ //Saturation Attribute
|
||||
def saturationValue = Math.round(convertHexToInt(descMap.value) / 255 * 100)
|
||||
log.debug "Saturation from refresh is $saturationValue"
|
||||
sendEvent(name: "saturation", value: saturationValue, displayed:false)
|
||||
}
|
||||
}
|
||||
else if(descMap.cluster == "0008"){
|
||||
def dimmerValue = Math.round(convertHexToInt(descMap.value) * 100 / 255)
|
||||
log.debug "dimmer value is $dimmerValue"
|
||||
sendEvent(name: "level", value: dimmerValue)
|
||||
def event = zigbee.getEvent(description)
|
||||
if (event) {
|
||||
log.debug event
|
||||
if (event.name=="level" && event.value==0) {}
|
||||
else {
|
||||
sendEvent(event)
|
||||
}
|
||||
}
|
||||
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
|
||||
def zigbeeMap = zigbee.parseDescriptionAsMap(description)
|
||||
def cluster = zigbee.parse(description)
|
||||
|
||||
if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) {
|
||||
if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute
|
||||
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||
sendEvent(name: "hue", value: hueValue, descriptionText: "Color has changed")
|
||||
}
|
||||
else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute
|
||||
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||
sendEvent(name: "saturation", value: saturationValue, descriptionText: "Color has changed", displayed: false)
|
||||
}
|
||||
}
|
||||
else if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) {
|
||||
if (cluster.data[0] == 0x00){
|
||||
log.debug "ON/OFF REPORTING CONFIG RESPONSE: $cluster"
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
else {
|
||||
log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.info "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug zigbeeMap
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
def on() {
|
||||
log.debug "on()"
|
||||
sendEvent(name: "switch", value: "on")
|
||||
setLevel(state?.levelValue)
|
||||
}
|
||||
|
||||
def zigbeeOff() {
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
|
||||
zigbee.on()
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "off()"
|
||||
sendEvent(name: "switch", value: "off")
|
||||
zigbeeOff()
|
||||
zigbee.off()
|
||||
}
|
||||
/**
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
return zigbee.onOffRefresh()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 1"
|
||||
]
|
||||
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01)
|
||||
}
|
||||
|
||||
def configure() {
|
||||
state.levelValue = 100
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
def configCmds = [
|
||||
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||
sendEvent(name: "checkInterval", value: 3 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
|
||||
//Switch Reporting
|
||||
"zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1000",
|
||||
|
||||
//Level Control Reporting
|
||||
"zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x0300 {${device.zigbeeId}} {}", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
|
||||
def poll(){
|
||||
log.debug "Poll is calling refresh"
|
||||
refresh()
|
||||
}
|
||||
|
||||
def zigbeeSetLevel(level) {
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}"
|
||||
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
state.levelValue = (value==null) ? 100 : value
|
||||
log.trace "setLevel($value)"
|
||||
def cmds = []
|
||||
|
||||
if (value == 0) {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
cmds << zigbeeOff()
|
||||
}
|
||||
else if (device.latestValue("switch") == "off") {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
|
||||
sendEvent(name: "level", value: state.levelValue)
|
||||
def level = hex(state.levelValue * 255 / 100)
|
||||
cmds << zigbeeSetLevel(level)
|
||||
|
||||
//log.debug cmds
|
||||
cmds
|
||||
}
|
||||
|
||||
//input Hue Integer values; returns color name for saturation 100%
|
||||
private getColorName(hueValue){
|
||||
if(hueValue>360 || hueValue<0)
|
||||
return
|
||||
|
||||
hueValue = Math.round(hueValue / 100 * 360)
|
||||
|
||||
log.debug "hue value is $hueValue"
|
||||
|
||||
def colorName = "Color Mode"
|
||||
if(hueValue>=0 && hueValue <= 4){
|
||||
colorName = "Red"
|
||||
}
|
||||
else if (hueValue>=5 && hueValue <=21 ){
|
||||
colorName = "Brick Red"
|
||||
}
|
||||
else if (hueValue>=22 && hueValue <=30 ){
|
||||
colorName = "Safety Orange"
|
||||
}
|
||||
else if (hueValue>=31 && hueValue <=40 ){
|
||||
colorName = "Dark Orange"
|
||||
}
|
||||
else if (hueValue>=41 && hueValue <=49 ){
|
||||
colorName = "Amber"
|
||||
}
|
||||
else if (hueValue>=50 && hueValue <=56 ){
|
||||
colorName = "Gold"
|
||||
}
|
||||
else if (hueValue>=57 && hueValue <=65 ){
|
||||
colorName = "Yellow"
|
||||
}
|
||||
else if (hueValue>=66 && hueValue <=83 ){
|
||||
colorName = "Electric Lime"
|
||||
}
|
||||
else if (hueValue>=84 && hueValue <=93 ){
|
||||
colorName = "Lawn Green"
|
||||
}
|
||||
else if (hueValue>=94 && hueValue <=112 ){
|
||||
colorName = "Bright Green"
|
||||
}
|
||||
else if (hueValue>=113 && hueValue <=135 ){
|
||||
colorName = "Lime"
|
||||
}
|
||||
else if (hueValue>=136 && hueValue <=166 ){
|
||||
colorName = "Spring Green"
|
||||
}
|
||||
else if (hueValue>=167 && hueValue <=171 ){
|
||||
colorName = "Turquoise"
|
||||
}
|
||||
else if (hueValue>=172 && hueValue <=187 ){
|
||||
colorName = "Aqua"
|
||||
}
|
||||
else if (hueValue>=188 && hueValue <=203 ){
|
||||
colorName = "Sky Blue"
|
||||
}
|
||||
else if (hueValue>=204 && hueValue <=217 ){
|
||||
colorName = "Dodger Blue"
|
||||
}
|
||||
else if (hueValue>=218 && hueValue <=223 ){
|
||||
colorName = "Navy Blue"
|
||||
}
|
||||
else if (hueValue>=224 && hueValue <=251 ){
|
||||
colorName = "Blue"
|
||||
}
|
||||
else if (hueValue>=252 && hueValue <=256 ){
|
||||
colorName = "Han Purple"
|
||||
}
|
||||
else if (hueValue>=257 && hueValue <=274 ){
|
||||
colorName = "Electric Indigo"
|
||||
}
|
||||
else if (hueValue>=275 && hueValue <=289 ){
|
||||
colorName = "Electric Purple"
|
||||
}
|
||||
else if (hueValue>=290 && hueValue <=300 ){
|
||||
colorName = "Orchid Purple"
|
||||
}
|
||||
else if (hueValue>=301 && hueValue <=315 ){
|
||||
colorName = "Magenta"
|
||||
}
|
||||
else if (hueValue>=316 && hueValue <=326 ){
|
||||
colorName = "Hot Pink"
|
||||
}
|
||||
else if (hueValue>=327 && hueValue <=335 ){
|
||||
colorName = "Deep Pink"
|
||||
}
|
||||
else if (hueValue>=336 && hueValue <=339 ){
|
||||
colorName = "Raspberry"
|
||||
}
|
||||
else if (hueValue>=340 && hueValue <=352 ){
|
||||
colorName = "Crimson"
|
||||
}
|
||||
else if (hueValue>=353 && hueValue <=360 ){
|
||||
colorName = "Red"
|
||||
}
|
||||
|
||||
colorName
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
|
||||
private hex(value, width=2) {
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() < width) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
private evenHex(value){
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() % 2 != 0) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
//Need to reverse array of size 2
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
byte tmp;
|
||||
tmp = array[1];
|
||||
array[1] = array[0];
|
||||
array[0] = tmp;
|
||||
return array
|
||||
}
|
||||
|
||||
def setAdjustedColor(value) {
|
||||
log.debug "setAdjustedColor: ${value}"
|
||||
def adjusted = value + [:]
|
||||
adjusted.level = null // needed because color picker always sends 100
|
||||
setColor(adjusted)
|
||||
zigbee.setLevel(value)
|
||||
}
|
||||
|
||||
def setColor(value){
|
||||
log.trace "setColor($value)"
|
||||
def max = 0xfe
|
||||
|
||||
if (value.hex) { sendEvent(name: "color", value: value.hex, displayed:false)}
|
||||
|
||||
def colorName = getColorName(value.hue)
|
||||
sendEvent(name: "colorName", value: colorName)
|
||||
|
||||
log.debug "color name is : $colorName"
|
||||
sendEvent(name: "hue", value: value.hue, displayed:false)
|
||||
sendEvent(name: "saturation", value: value.saturation, displayed:false)
|
||||
def scaledHueValue = evenHex(Math.round(value.hue * max / 100.0))
|
||||
def scaledSatValue = evenHex(Math.round(value.saturation * max / 100.0))
|
||||
|
||||
def cmd = []
|
||||
if (value.switch != "off" && device.latestValue("switch") == "off") {
|
||||
cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}"
|
||||
cmd << "delay 150"
|
||||
zigbee.on() + setHue(value.hue) + "delay 500" + setSaturation(value.saturation)
|
||||
}
|
||||
|
||||
cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x00 {${scaledHueValue} 00 0000}"
|
||||
cmd << "delay 150"
|
||||
cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x03 {${scaledSatValue} 0000}"
|
||||
|
||||
if (value.level) {
|
||||
state.levelValue = value.level
|
||||
sendEvent(name: "level", value: value.level)
|
||||
def level = hex(value.level * 255 / 100)
|
||||
cmd << zigbeeSetLevel(level)
|
||||
def setHue(value) {
|
||||
def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5)
|
||||
}
|
||||
|
||||
if (value.switch == "off") {
|
||||
cmd << "delay 150"
|
||||
cmd << off()
|
||||
}
|
||||
|
||||
cmd
|
||||
def setSaturation(value) {
|
||||
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + "delay 1000" + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
}
|
||||
|
||||
@@ -29,431 +29,155 @@ metadata {
|
||||
|
||||
}
|
||||
|
||||
// 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", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821"
|
||||
state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff"
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel"
|
||||
}
|
||||
tileAttribute ("device.color", key: "COLOR_CONTROL") {
|
||||
attributeState "color", action:"color control.setColor"
|
||||
}
|
||||
}
|
||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") {
|
||||
state "colorTemperature", action:"color temperature.setColorTemperature"
|
||||
}
|
||||
valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "colorName", label: '${currentValue}'
|
||||
}
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") {
|
||||
state "colorTemperature", action:"color temperature.setColorTemperature"
|
||||
}
|
||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") {
|
||||
state "colorTemperature", label: '${currentValue} K'
|
||||
}
|
||||
valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat") {
|
||||
state "colorName", label: '${currentValue}'
|
||||
}
|
||||
|
||||
controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) {
|
||||
state "color", action:"setAdjustedColor"
|
||||
}
|
||||
|
||||
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", "refresh", "colorName", "levelSliderControl", "level", "colorTempSliderControl", "colorTemp", "rgbSelector"])
|
||||
details(["switch", "colorTempSliderControl", "colorName", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
//Globals
|
||||
private getATTRIBUTE_HUE() { 0x0000 }
|
||||
private getATTRIBUTE_SATURATION() { 0x0001 }
|
||||
private getHUE_COMMAND() { 0x00 }
|
||||
private getSATURATION_COMMAND() { 0x03 }
|
||||
private getCOLOR_CONTROL_CLUSTER() { 0x0300 }
|
||||
private getATTRIBUTE_COLOR_TEMPERATURE() { 0x0007 }
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
//log.info "description is $description"
|
||||
if (description?.startsWith("catchall:")) {
|
||||
if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1"))
|
||||
{
|
||||
def result = createEvent(name: "switch", value: "on")
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
}
|
||||
else if(description?.endsWith("0000") || description?.endsWith("1000") || description?.matches("on/off\\s*:\\s*0"))
|
||||
{
|
||||
if(!(description?.startsWith("catchall: 0104 0300"))){
|
||||
def result = createEvent(name: "switch", value: "off")
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
}
|
||||
}
|
||||
log.debug "description is $description"
|
||||
|
||||
def event = zigbee.getEvent(description)
|
||||
if (event) {
|
||||
log.debug event
|
||||
if (event.name=="level" && event.value==0) {}
|
||||
else {
|
||||
if (event.name=="colorTemperature") {
|
||||
setGenericName(event.value)
|
||||
}
|
||||
else if (description?.startsWith("read attr -")) { //for values returned after hitting refresh
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
log.trace "descMap : $descMap"
|
||||
|
||||
if (descMap.cluster == "0300") {
|
||||
if(descMap.attrId == "0007"){
|
||||
log.debug "in read attr"
|
||||
log.debug descMap.value
|
||||
def tempInMired = convertHexToInt(descMap.value)
|
||||
def tempInKelvin = Math.round(1000000/tempInMired)
|
||||
log.trace "temp in kelvin: $tempInKelvin"
|
||||
sendEvent(name: "colorTemperature", value: tempInKelvin, displayed:false)
|
||||
}
|
||||
else if(descMap.attrId == "0008"){ //Color mode attribute
|
||||
if(descMap.value == "00"){
|
||||
state.colorType = "rgb"
|
||||
}else if(descMap.value == "02"){
|
||||
state.colorType = "white"
|
||||
}
|
||||
}
|
||||
else if(descMap.attrId == "0000"){ //Hue Attribute
|
||||
def hueValue = Math.round(convertHexToInt(descMap.value) / 255 * 360)
|
||||
log.debug "Hue value returned is $hueValue"
|
||||
sendEvent(name: "hue", value: hueValue, displayed:false)
|
||||
}
|
||||
else if(descMap.attrId == "0001"){ //Saturation Attribute
|
||||
def saturationValue = Math.round(convertHexToInt(descMap.value) / 255 * 100)
|
||||
log.debug "Saturation from refresh is $saturationValue"
|
||||
sendEvent(name: "saturation", value: saturationValue, displayed:false)
|
||||
}
|
||||
}
|
||||
else if(descMap.cluster == "0008"){
|
||||
def dimmerValue = Math.round(convertHexToInt(descMap.value) * 100 / 255)
|
||||
log.debug "dimmer value is $dimmerValue"
|
||||
sendEvent(name: "level", value: dimmerValue)
|
||||
sendEvent(event)
|
||||
}
|
||||
}
|
||||
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 "description is $description"
|
||||
return result
|
||||
def zigbeeMap = zigbee.parseDescriptionAsMap(description)
|
||||
def cluster = zigbee.parse(description)
|
||||
|
||||
if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) {
|
||||
if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute
|
||||
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||
sendEvent(name: "hue", value: hueValue, descriptionText: "Color has changed")
|
||||
}
|
||||
else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute
|
||||
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||
sendEvent(name: "saturation", value: saturationValue, descriptionText: "Color has changed", displayed: false)
|
||||
}
|
||||
}
|
||||
else if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) {
|
||||
if (cluster.data[0] == 0x00){
|
||||
log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
else {
|
||||
log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.info "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug zigbeeMap
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
def on() {
|
||||
log.debug "on()"
|
||||
sendEvent(name: "switch", value: "on")
|
||||
setLevel(state?.levelValue)
|
||||
}
|
||||
|
||||
def zigbeeOff() {
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
|
||||
zigbee.on()
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "off()"
|
||||
sendEvent(name: "switch", value: "off")
|
||||
zigbeeOff()
|
||||
zigbee.off()
|
||||
}
|
||||
/**
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
return zigbee.onOffRefresh()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 1", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 7", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 8"
|
||||
]
|
||||
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01)
|
||||
}
|
||||
|
||||
def configure() {
|
||||
state.levelValue = 100
|
||||
state.colorType = "white"
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
def configCmds = [
|
||||
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||
sendEvent(name: "checkInterval", value: 3 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
|
||||
//Switch Reporting
|
||||
"zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1000",
|
||||
|
||||
//Level Control Reporting
|
||||
"zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x0300 {${device.zigbeeId}} {}", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def setColorTemperature(value) {
|
||||
state?.colorType = "white"
|
||||
if(value<101){
|
||||
value = (value*38) + 2700 //Calculation of mapping 0-100 to 2700-6500
|
||||
}
|
||||
|
||||
def tempInMired = Math.round(1000000/value)
|
||||
def finalHex = swapEndianHex(hex(tempInMired, 4))
|
||||
def genericName = getGenericName(value)
|
||||
log.debug "generic name is : $genericName"
|
||||
|
||||
def cmds = []
|
||||
sendEvent(name: "colorTemperature", value: value, displayed:false)
|
||||
sendEvent(name: "colorName", value: genericName)
|
||||
|
||||
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0300 0x0a {${finalHex} 2000}"
|
||||
|
||||
cmds
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
|
||||
def poll(){
|
||||
log.debug "Poll is calling refresh"
|
||||
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
refresh()
|
||||
}
|
||||
|
||||
def zigbeeSetLevel(level) {
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}"
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
state.levelValue = (value==null) ? 100 : value
|
||||
log.trace "setLevel($value)"
|
||||
def cmds = []
|
||||
|
||||
if (value == 0) {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
cmds << zigbeeOff()
|
||||
}
|
||||
else if (device.latestValue("switch") == "off") {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
|
||||
sendEvent(name: "level", value: state.levelValue)
|
||||
def level = hex(state.levelValue * 255 / 100)
|
||||
cmds << zigbeeSetLevel(level)
|
||||
|
||||
//log.debug cmds
|
||||
cmds
|
||||
def setColorTemperature(value) {
|
||||
setGenericName(value)
|
||||
zigbee.setColorTemperature(value)
|
||||
}
|
||||
|
||||
//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature
|
||||
private getGenericName(value){
|
||||
def setGenericName(value){
|
||||
if (value != null) {
|
||||
def genericName = "White"
|
||||
if(state?.colorType == "rgb"){
|
||||
genericName = "Color Mode"
|
||||
}
|
||||
else{
|
||||
if (value < 3300) {
|
||||
genericName = "Soft White"
|
||||
} else if (value < 4150) {
|
||||
genericName = "Moonlight"
|
||||
} else if(value < 5000){
|
||||
} else if (value <= 5000) {
|
||||
genericName = "Cool White"
|
||||
} else if(value <= 6500){
|
||||
} else if (value >= 5000) {
|
||||
genericName = "Daylight"
|
||||
}
|
||||
sendEvent(name: "colorName", value: genericName)
|
||||
}
|
||||
}
|
||||
|
||||
genericName
|
||||
}
|
||||
|
||||
//input Hue Integer values; returns color name for saturation 100%
|
||||
private getColorName(hueValue){
|
||||
if(hueValue>360 || hueValue<0)
|
||||
return
|
||||
|
||||
hueValue = Math.round(hueValue / 100 * 360)
|
||||
|
||||
log.debug "hue value is $hueValue"
|
||||
|
||||
def colorName = "Color Mode"
|
||||
if(hueValue>=0 && hueValue <= 4){
|
||||
colorName = "Red"
|
||||
}
|
||||
else if (hueValue>=5 && hueValue <=21 ){
|
||||
colorName = "Brick Red"
|
||||
}
|
||||
else if (hueValue>=22 && hueValue <=30 ){
|
||||
colorName = "Safety Orange"
|
||||
}
|
||||
else if (hueValue>=31 && hueValue <=40 ){
|
||||
colorName = "Dark Orange"
|
||||
}
|
||||
else if (hueValue>=41 && hueValue <=49 ){
|
||||
colorName = "Amber"
|
||||
}
|
||||
else if (hueValue>=50 && hueValue <=56 ){
|
||||
colorName = "Gold"
|
||||
}
|
||||
else if (hueValue>=57 && hueValue <=65 ){
|
||||
colorName = "Yellow"
|
||||
}
|
||||
else if (hueValue>=66 && hueValue <=83 ){
|
||||
colorName = "Electric Lime"
|
||||
}
|
||||
else if (hueValue>=84 && hueValue <=93 ){
|
||||
colorName = "Lawn Green"
|
||||
}
|
||||
else if (hueValue>=94 && hueValue <=112 ){
|
||||
colorName = "Bright Green"
|
||||
}
|
||||
else if (hueValue>=113 && hueValue <=135 ){
|
||||
colorName = "Lime"
|
||||
}
|
||||
else if (hueValue>=136 && hueValue <=166 ){
|
||||
colorName = "Spring Green"
|
||||
}
|
||||
else if (hueValue>=167 && hueValue <=171 ){
|
||||
colorName = "Turquoise"
|
||||
}
|
||||
else if (hueValue>=172 && hueValue <=187 ){
|
||||
colorName = "Aqua"
|
||||
}
|
||||
else if (hueValue>=188 && hueValue <=203 ){
|
||||
colorName = "Sky Blue"
|
||||
}
|
||||
else if (hueValue>=204 && hueValue <=217 ){
|
||||
colorName = "Dodger Blue"
|
||||
}
|
||||
else if (hueValue>=218 && hueValue <=223 ){
|
||||
colorName = "Navy Blue"
|
||||
}
|
||||
else if (hueValue>=224 && hueValue <=251 ){
|
||||
colorName = "Blue"
|
||||
}
|
||||
else if (hueValue>=252 && hueValue <=256 ){
|
||||
colorName = "Han Purple"
|
||||
}
|
||||
else if (hueValue>=257 && hueValue <=274 ){
|
||||
colorName = "Electric Indigo"
|
||||
}
|
||||
else if (hueValue>=275 && hueValue <=289 ){
|
||||
colorName = "Electric Purple"
|
||||
}
|
||||
else if (hueValue>=290 && hueValue <=300 ){
|
||||
colorName = "Orchid Purple"
|
||||
}
|
||||
else if (hueValue>=301 && hueValue <=315 ){
|
||||
colorName = "Magenta"
|
||||
}
|
||||
else if (hueValue>=316 && hueValue <=326 ){
|
||||
colorName = "Hot Pink"
|
||||
}
|
||||
else if (hueValue>=327 && hueValue <=335 ){
|
||||
colorName = "Deep Pink"
|
||||
}
|
||||
else if (hueValue>=336 && hueValue <=339 ){
|
||||
colorName = "Raspberry"
|
||||
}
|
||||
else if (hueValue>=340 && hueValue <=352 ){
|
||||
colorName = "Crimson"
|
||||
}
|
||||
else if (hueValue>=353 && hueValue <=360 ){
|
||||
colorName = "Red"
|
||||
}
|
||||
|
||||
colorName
|
||||
}
|
||||
|
||||
|
||||
private getEndpointId() {
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
|
||||
private hex(value, width=2) {
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() < width) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
private evenHex(value){
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() % 2 != 0) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
//Need to reverse array of size 2
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
byte tmp;
|
||||
tmp = array[1];
|
||||
array[1] = array[0];
|
||||
array[0] = tmp;
|
||||
return array
|
||||
}
|
||||
|
||||
def setAdjustedColor(value) {
|
||||
log.debug "setAdjustedColor: ${value}"
|
||||
def adjusted = value + [:]
|
||||
adjusted.level = null // needed because color picker always sends 100
|
||||
setColor(adjusted)
|
||||
def setLevel(value) {
|
||||
zigbee.setLevel(value)
|
||||
}
|
||||
|
||||
def setColor(value){
|
||||
state?.colorType = "rgb"
|
||||
log.trace "setColor($value)"
|
||||
def max = 0xfe
|
||||
|
||||
if (value.hex) { sendEvent(name: "color", value: value.hex, displayed:false)}
|
||||
|
||||
def colorName = getColorName(value.hue)
|
||||
log.debug "color name is : $colorName"
|
||||
sendEvent(name: "colorName", value: colorName)
|
||||
sendEvent(name: "colorTemperature", value: "--", displayed:false)
|
||||
|
||||
|
||||
sendEvent(name: "hue", value: value.hue, displayed:false)
|
||||
sendEvent(name: "saturation", value: value.saturation, displayed:false)
|
||||
def scaledHueValue = evenHex(Math.round(value.hue * max / 100.0))
|
||||
def scaledSatValue = evenHex(Math.round(value.saturation * max / 100.0))
|
||||
|
||||
def cmd = []
|
||||
if (value.switch != "off" && device.latestValue("switch") == "off") {
|
||||
cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}"
|
||||
cmd << "delay 150"
|
||||
zigbee.on() + setHue(value.hue) + "delay 300" + setSaturation(value.saturation)
|
||||
}
|
||||
|
||||
cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x00 {${scaledHueValue} 00 0000}"
|
||||
cmd << "delay 150"
|
||||
cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x03 {${scaledSatValue} 0000}"
|
||||
|
||||
if (value.level) {
|
||||
state.levelValue = value.level
|
||||
sendEvent(name: "level", value: value.level)
|
||||
def level = hex(value.level * 255 / 100)
|
||||
cmd << zigbeeSetLevel(level)
|
||||
def setHue(value) {
|
||||
def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5)
|
||||
}
|
||||
|
||||
if (value.switch == "off") {
|
||||
cmd << "delay 150"
|
||||
cmd << off()
|
||||
}
|
||||
|
||||
cmd
|
||||
def setSaturation(value) {
|
||||
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + "delay 1000" + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
}
|
||||
@@ -21,232 +21,119 @@ metadata {
|
||||
attribute "colorName", "string"
|
||||
}
|
||||
|
||||
// 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", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821"
|
||||
state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff"
|
||||
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") {
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel"
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") {
|
||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") {
|
||||
state "colorTemperature", action:"color temperature.setColorTemperature"
|
||||
}
|
||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") {
|
||||
state "colorTemperature", label: '${currentValue} K'
|
||||
}
|
||||
valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat") {
|
||||
valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "colorName", label: '${currentValue}'
|
||||
}
|
||||
|
||||
|
||||
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", "refresh", "colorName", "levelSliderControl", "level", "colorTempSliderControl", "colorTemp"])
|
||||
details(["switch", "colorTempSliderControl", "colorName", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
//log.trace description
|
||||
|
||||
if (description?.startsWith("catchall:")) {
|
||||
if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1"))
|
||||
{
|
||||
def result = createEvent(name: "switch", value: "on")
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
log.debug "description is $description"
|
||||
def event = zigbee.getEvent(description)
|
||||
if (event) {
|
||||
if (event.name=="level" && event.value==0) {}
|
||||
else {
|
||||
if (event.name=="colorTemperature") {
|
||||
setGenericName(event.value)
|
||||
}
|
||||
else if(description?.endsWith("0000") || description?.endsWith("1000") || description?.matches("on/off\\s*:\\s*0"))
|
||||
{
|
||||
def result = createEvent(name: "switch", value: "off")
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
else if (description?.startsWith("read attr -")) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
log.trace "descMap : $descMap"
|
||||
|
||||
if (descMap.cluster == "0300") {
|
||||
log.debug descMap.value
|
||||
def tempInMired = convertHexToInt(descMap.value)
|
||||
def tempInKelvin = Math.round(1000000/tempInMired)
|
||||
log.trace "temp in kelvin: $tempInKelvin"
|
||||
sendEvent(name: "colorTemperature", value: tempInKelvin, displayed:false)
|
||||
}
|
||||
else if(descMap.cluster == "0008"){
|
||||
def dimmerValue = Math.round(convertHexToInt(descMap.value) * 100 / 255)
|
||||
log.debug "dimmer value is $dimmerValue"
|
||||
sendEvent(name: "level", value: dimmerValue)
|
||||
sendEvent(event)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
def cluster = zigbee.parse(description)
|
||||
|
||||
def on() {
|
||||
log.debug "on()"
|
||||
sendEvent(name: "switch", value: "on")
|
||||
setLevel(state?.levelValue)
|
||||
if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) {
|
||||
if (cluster.data[0] == 0x00) {
|
||||
log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
else {
|
||||
log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug "${cluster}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "off()"
|
||||
sendEvent(name: "switch", value: "off")
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
|
||||
zigbee.off()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 7"
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
def configure() {
|
||||
state.levelValue = 100
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
def configCmds = [
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x0300 {${device.zigbeeId}} {}", "delay 500"
|
||||
]
|
||||
return onOffConfig() + levelConfig() + configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def onOffConfig() {
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 6 0 0x10 0 300 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||
]
|
||||
}
|
||||
|
||||
//level config for devices with min reporting interval as 5 seconds and reporting interval if no activity as 1hour (3600s)
|
||||
//min level change is 01
|
||||
def levelConfig() {
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 8 0 0x20 5 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500"
|
||||
]
|
||||
}
|
||||
|
||||
def setColorTemperature(value) {
|
||||
if(value<101){
|
||||
value = (value*38) + 2700 //Calculation of mapping 0-100 to 2700-6500
|
||||
}
|
||||
|
||||
def tempInMired = Math.round(1000000/value)
|
||||
def finalHex = swapEndianHex(hex(tempInMired, 4))
|
||||
def genericName = getGenericName(value)
|
||||
log.debug "generic name is : $genericName"
|
||||
|
||||
def cmds = []
|
||||
sendEvent(name: "colorTemperature", value: value, displayed:false)
|
||||
sendEvent(name: "colorName", value: genericName)
|
||||
|
||||
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0300 0x0a {${finalHex} 2000}"
|
||||
|
||||
cmds
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
def on() {
|
||||
zigbee.on()
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
state.levelValue = (value==null) ? 100 : 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")
|
||||
zigbee.setLevel(value)
|
||||
}
|
||||
|
||||
sendEvent(name: "level", value: state.levelValue)
|
||||
def level = hex(state.levelValue * 254 / 100)
|
||||
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}"
|
||||
/**
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
return zigbee.onOffRefresh()
|
||||
}
|
||||
|
||||
//log.debug cmds
|
||||
cmds
|
||||
def refresh() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||
sendEvent(name: "checkInterval", value: 3 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
|
||||
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
refresh()
|
||||
}
|
||||
|
||||
def setColorTemperature(value) {
|
||||
setGenericName(value)
|
||||
zigbee.setColorTemperature(value)
|
||||
}
|
||||
|
||||
//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature
|
||||
private getGenericName(value){
|
||||
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){
|
||||
} else if (value <= 5000) {
|
||||
genericName = "Cool White"
|
||||
} else if(value <= 6500){
|
||||
} else if (value >= 5000) {
|
||||
genericName = "Daylight"
|
||||
}
|
||||
|
||||
genericName
|
||||
sendEvent(name: "colorName", value: genericName)
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
|
||||
private hex(value, width=2) {
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() < width) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
//Need to reverse array of size 2
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
byte tmp;
|
||||
tmp = array[1];
|
||||
array[1] = array[0];
|
||||
array[0] = tmp;
|
||||
return array
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ def refresh() {
|
||||
}
|
||||
|
||||
def configure() {
|
||||
onOffConfig() + levelConfig() + powerConfig() + refresh()
|
||||
refresh() + onOffConfig() + levelConfig() + powerConfig()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -47,9 +47,21 @@ def parse(String description) {
|
||||
|
||||
// Commands to device
|
||||
def on() {
|
||||
'zcl on-off on'
|
||||
[
|
||||
'zcl on-off on',
|
||||
'delay 200',
|
||||
"send 0x${zigbee.deviceNetworkId} 0x01 0x${zigbee.endpointId}",
|
||||
'delay 500'
|
||||
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
def off() {
|
||||
'zcl on-off off'
|
||||
[
|
||||
'zcl on-off off',
|
||||
'delay 200',
|
||||
"send 0x${zigbee.deviceNetworkId} 0x01 0x${zigbee.endpointId}",
|
||||
'delay 500'
|
||||
]
|
||||
}
|
||||
|
||||
@@ -23,10 +23,10 @@ Works with:
|
||||
|
||||
## Device Health
|
||||
|
||||
A Category C1 smart power outlet with maxReportTime of 10 min.
|
||||
Check-in interval is double the value of maxReportTime for Zigbee device.
|
||||
A Category C1 smart power outlet with maxReportTime of 5 mins.
|
||||
Check-in interval is double the value of maxReportTime.
|
||||
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||
Check-in interval = 2*10 = 20 min
|
||||
Check-in interval = 12 mins
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
metadata {
|
||||
// Automatically generated. Make future change here.
|
||||
definition (name: "SmartPower Outlet", namespace: "smartthings", author: "SmartThings", category: "C1") {
|
||||
definition (name: "SmartPower Outlet", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Switch"
|
||||
capability "Power Meter"
|
||||
@@ -101,17 +101,24 @@ def parse(String description) {
|
||||
else {
|
||||
def descriptionText = finalResult.value == "on" ? '{{ device.displayName }} is On' : '{{ device.displayName }} is Off'
|
||||
sendEvent(name: finalResult.type, value: finalResult.value, descriptionText: descriptionText, translatable: true)
|
||||
// Temporary fix for the case when Device is OFFLINE and is connected again
|
||||
if (state.lastActivity == null){
|
||||
state.lastActivity = now()
|
||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
||||
}
|
||||
state.lastActivity = now()
|
||||
}
|
||||
else {
|
||||
def cluster = zigbee.parse(description)
|
||||
|
||||
if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07){
|
||||
if (cluster.data[0] == 0x00) {
|
||||
log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
else {
|
||||
log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug zigbee.parseDescriptionAsMap(description)
|
||||
log.debug "${cluster}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,15 +133,7 @@ def on() {
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
|
||||
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
|
||||
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
|
||||
state.lastActivity = null
|
||||
return zigbee.onOffRefresh()
|
||||
} else {
|
||||
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
|
||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
||||
}
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
@@ -142,8 +141,12 @@ def refresh() {
|
||||
}
|
||||
|
||||
def configure() {
|
||||
sendEvent(name: "checkInterval", value: 1200, displayed: false, data: [protocol: "zigbee"])
|
||||
zigbee.onOffConfig() + powerConfig() + refresh()
|
||||
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||
sendEvent(name: "checkInterval", value: 3 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
|
||||
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
refresh() + zigbee.onOffConfig(0, 300) + powerConfig()
|
||||
}
|
||||
|
||||
//power config for devices with min reporting interval as 1 seconds and reporting interval if no activity as 10min (600s)
|
||||
|
||||
@@ -23,10 +23,10 @@ Works with:
|
||||
|
||||
## Device Health
|
||||
|
||||
A Category C2 moisture sensor with maxReportTime of 1 hr.
|
||||
Check-in interval is double the value of maxReportTime for Zigbee device.
|
||||
A Category C2 moisture sensor with maxReportTime of 5 mins.
|
||||
Check-in interval is double the value of maxReportTime.
|
||||
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||
Check-in interval = 2*60 = 120 min
|
||||
Check-in interval = 12 mins
|
||||
|
||||
## Battery Specification
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
|
||||
|
||||
|
||||
metadata {
|
||||
definition (name: "SmartSense Moisture Sensor",namespace: "smartthings", author: "SmartThings", category: "C2") {
|
||||
definition (name: "SmartSense Moisture Sensor",namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Configuration"
|
||||
capability "Battery"
|
||||
capability "Refresh"
|
||||
@@ -101,13 +101,6 @@ def parse(String description) {
|
||||
map = parseIasMessage(description)
|
||||
}
|
||||
|
||||
// Temporary fix for the case when Device is OFFLINE and is connected again
|
||||
if (state.lastActivity == null){
|
||||
state.lastActivity = now()
|
||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
||||
}
|
||||
state.lastActivity = now()
|
||||
|
||||
log.debug "Parse returned $map"
|
||||
def result = map ? createEvent(map) : null
|
||||
|
||||
@@ -125,14 +118,28 @@ private Map parseCatchAllMessage(String description) {
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0001:
|
||||
// 0x07 - configure reporting
|
||||
if (cluster.command != 0x07) {
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
}
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
if (cluster.command == 0x07) {
|
||||
if (cluster.data[0] == 0x00){
|
||||
log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
else {
|
||||
log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||
}
|
||||
}
|
||||
else {
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -142,10 +149,8 @@ private Map parseCatchAllMessage(String description) {
|
||||
|
||||
private boolean shouldProcessMessage(cluster) {
|
||||
// 0x0B is default response indicating message got through
|
||||
// 0x07 is bind message
|
||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||
cluster.command == 0x0B ||
|
||||
cluster.command == 0x07 ||
|
||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||
return !ignoredMessage
|
||||
}
|
||||
@@ -187,9 +192,9 @@ private Map parseIasMessage(String description) {
|
||||
def getTemperature(value) {
|
||||
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
||||
if(getTemperatureScale() == "C"){
|
||||
return celsius
|
||||
return Math.round(celsius)
|
||||
} else {
|
||||
return celsiusToFahrenheit(celsius) as Integer
|
||||
return Math.round(celsiusToFahrenheit(celsius))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,20 +202,14 @@ private Map getBatteryResult(rawValue) {
|
||||
log.debug "Battery rawValue = ${rawValue}"
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: '--',
|
||||
translatable: true
|
||||
]
|
||||
def result = [:]
|
||||
|
||||
def volts = rawValue / 10
|
||||
|
||||
if (rawValue == 0 || rawValue == 255) {}
|
||||
else {
|
||||
if (volts > 3.5) {
|
||||
result.descriptionText = "{{ device.displayName }} battery has too much power: (> 3.5) volts."
|
||||
}
|
||||
else {
|
||||
if (!(rawValue == 0 || rawValue == 255)) {
|
||||
result.name = 'battery'
|
||||
result.translatable = true
|
||||
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||
volts = rawValue // For the batteryMap to work the key needs to be an int
|
||||
def batteryMap = [28: 100, 27: 100, 26: 100, 25: 90, 24: 90, 23: 70,
|
||||
@@ -223,20 +222,17 @@ private Map getBatteryResult(rawValue) {
|
||||
else if (volts > maxVolts)
|
||||
volts = maxVolts
|
||||
def pct = batteryMap[volts]
|
||||
if (pct != null) {
|
||||
result.value = pct
|
||||
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
def roundedPct = Math.round(pct * 100)
|
||||
if (roundedPct <= 0)
|
||||
roundedPct = 1
|
||||
result.value = Math.min(100, roundedPct)
|
||||
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -283,15 +279,7 @@ private Map getMoistureResult(value) {
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
|
||||
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
|
||||
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
|
||||
state.lastActivity = null
|
||||
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
||||
} else {
|
||||
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
|
||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
||||
}
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
@@ -305,23 +293,13 @@ def refresh() {
|
||||
}
|
||||
|
||||
def configure() {
|
||||
sendEvent(name: "checkInterval", value: 14400, displayed: false, data: [protocol: "zigbee"])
|
||||
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
// battery minReport 30 seconds, maxReportTime 6 hrs by default
|
||||
return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
|
||||
@@ -22,10 +22,10 @@ Works with:
|
||||
|
||||
## Device Health
|
||||
|
||||
A Category C2 motion sensor with maxReportTime of 1 hr.
|
||||
Check-in interval is double the value of maxReportTime for Zigbee device.
|
||||
A Category C2 motion sensor with maxReportTime of 5 mins.
|
||||
Check-in interval is double the value of maxReportTime.
|
||||
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||
Check-in interval = 2*60 = 120 min
|
||||
Check-in interval = 12 mins
|
||||
|
||||
## Battery Specification
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
|
||||
|
||||
|
||||
metadata {
|
||||
definition (name: "SmartSense Motion Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
|
||||
definition (name: "SmartSense Motion Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Motion Sensor"
|
||||
capability "Configuration"
|
||||
capability "Battery"
|
||||
@@ -105,13 +105,6 @@ def parse(String description) {
|
||||
map = parseIasMessage(description)
|
||||
}
|
||||
|
||||
// Temporary fix for the case when Device is OFFLINE and is connected again
|
||||
if (state.lastActivity == null){
|
||||
state.lastActivity = now()
|
||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
||||
}
|
||||
state.lastActivity = now()
|
||||
|
||||
log.debug "Parse returned $map"
|
||||
def result = map ? createEvent(map) : null
|
||||
|
||||
@@ -129,19 +122,37 @@ private Map parseCatchAllMessage(String description) {
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0001:
|
||||
// 0x07 - configure reporting
|
||||
if (cluster.command != 0x07) {
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
}
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
if (cluster.command == 0x07) {
|
||||
if (cluster.data[0] == 0x00) {
|
||||
log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
else {
|
||||
log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
}
|
||||
break
|
||||
|
||||
case 0x0406:
|
||||
// 0x07 - configure reporting
|
||||
if (cluster.command != 0x07) {
|
||||
log.debug 'motion'
|
||||
resultMap.name = 'motion'
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -151,10 +162,8 @@ private Map parseCatchAllMessage(String description) {
|
||||
|
||||
private boolean shouldProcessMessage(cluster) {
|
||||
// 0x0B is default response indicating message got through
|
||||
// 0x07 is bind message
|
||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||
cluster.command == 0x0B ||
|
||||
cluster.command == 0x07 ||
|
||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||
return !ignoredMessage
|
||||
}
|
||||
@@ -201,9 +210,9 @@ private Map parseIasMessage(String description) {
|
||||
def getTemperature(value) {
|
||||
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
||||
if(getTemperatureScale() == "C"){
|
||||
return celsius
|
||||
return Math.round(celsius)
|
||||
} else {
|
||||
return celsiusToFahrenheit(celsius) as Integer
|
||||
return Math.round(celsiusToFahrenheit(celsius))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,20 +220,14 @@ private Map getBatteryResult(rawValue) {
|
||||
log.debug "Battery rawValue = ${rawValue}"
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: '--',
|
||||
translatable: true
|
||||
]
|
||||
def result = [:]
|
||||
|
||||
def volts = rawValue / 10
|
||||
|
||||
if (rawValue == 0 || rawValue == 255) {}
|
||||
else {
|
||||
if (volts > 3.5) {
|
||||
result.descriptionText = "{{ device.displayName }} battery has too much power: (> 3.5) volts."
|
||||
}
|
||||
else {
|
||||
if (!(rawValue == 0 || rawValue == 255)) {
|
||||
result.name = 'battery'
|
||||
result.translatable = true
|
||||
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||
volts = rawValue // For the batteryMap to work the key needs to be an int
|
||||
def batteryMap = [28: 100, 27: 100, 26: 100, 25: 90, 24: 90, 23: 70,
|
||||
@@ -237,20 +240,15 @@ private Map getBatteryResult(rawValue) {
|
||||
else if (volts > maxVolts)
|
||||
volts = maxVolts
|
||||
def pct = batteryMap[volts]
|
||||
if (pct != null) {
|
||||
result.value = pct
|
||||
def value = pct
|
||||
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
def roundedPct = Math.round(pct * 100)
|
||||
if (roundedPct <= 0)
|
||||
roundedPct = 1
|
||||
result.value = Math.min(100, roundedPct)
|
||||
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,15 +292,7 @@ private Map getMotionResult(value) {
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
|
||||
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
|
||||
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
|
||||
state.lastActivity = null
|
||||
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
||||
} else {
|
||||
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
|
||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
||||
}
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
@@ -316,24 +306,13 @@ def refresh() {
|
||||
}
|
||||
|
||||
def configure() {
|
||||
sendEvent(name: "checkInterval", value: 14400, displayed: false, data: [protocol: "zigbee"])
|
||||
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
// battery minReport 30 seconds, maxReportTime 6 hrs by default
|
||||
return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
|
||||
@@ -30,15 +30,19 @@ metadata {
|
||||
|
||||
}
|
||||
|
||||
simulator {
|
||||
status "active": "zone report :: type: 19 value: 0031"
|
||||
status "inactive": "zone report :: type: 19 value: 0030"
|
||||
}
|
||||
|
||||
preferences {
|
||||
input title: "Temperature Offset", 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"
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"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 {
|
||||
input title: "Temperature Offset", 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: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
}
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"motion", type: "generic", width: 6, height: 4){
|
||||
@@ -106,19 +110,37 @@ private Map parseCatchAllMessage(String description) {
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0001:
|
||||
// 0x07 - configure reporting
|
||||
if (cluster.command != 0x07) {
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
}
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
if (cluster.command == 0x07) {
|
||||
if (cluster.data[0] == 0x00) {
|
||||
log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
else {
|
||||
log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
}
|
||||
break
|
||||
|
||||
case 0x0406:
|
||||
// 0x07 - configure reporting
|
||||
if (cluster.command != 0x07) {
|
||||
log.debug 'motion'
|
||||
resultMap.name = 'motion'
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -128,10 +150,8 @@ private Map parseCatchAllMessage(String description) {
|
||||
|
||||
private boolean shouldProcessMessage(cluster) {
|
||||
// 0x0B is default response indicating message got through
|
||||
// 0x07 is bind message
|
||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||
cluster.command == 0x0B ||
|
||||
cluster.command == 0x07 ||
|
||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||
return !ignoredMessage
|
||||
}
|
||||
@@ -170,15 +190,17 @@ private Map parseCustomMessage(String description) {
|
||||
|
||||
private Map parseIasMessage(String description) {
|
||||
ZoneStatus zs = zigbee.parseZoneStatus(description)
|
||||
|
||||
// Some sensor models that use this DTH use alarm1 and some use alarm2 to signify motion
|
||||
return (zs.isAlarm1Set() || zs.isAlarm2Set()) ? getMotionResult('active') : getMotionResult('inactive')
|
||||
}
|
||||
|
||||
def getTemperature(value) {
|
||||
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
||||
if(getTemperatureScale() == "C"){
|
||||
return celsius
|
||||
return Math.round(celsius)
|
||||
} else {
|
||||
return celsiusToFahrenheit(celsius) as Integer
|
||||
return Math.round(celsiusToFahrenheit(celsius))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,29 +208,21 @@ private Map getBatteryResult(rawValue) {
|
||||
log.debug 'Battery'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
log.debug rawValue
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: '--'
|
||||
]
|
||||
def result = [:]
|
||||
|
||||
def volts = rawValue / 10
|
||||
def descriptionText
|
||||
|
||||
if (rawValue == 0 || rawValue == 255) {}
|
||||
else {
|
||||
if (volts > 3.5) {
|
||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||
}
|
||||
else if (volts > 0){
|
||||
if (!(rawValue == 0 || rawValue == 255)) {
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
def roundedPct = Math.round(pct * 100)
|
||||
if (roundedPct <= 0)
|
||||
roundedPct = 1
|
||||
result.name = 'battery'
|
||||
result.value = Math.min(100, roundedPct)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -216,32 +230,44 @@ private Map getBatteryResult(rawValue) {
|
||||
|
||||
private Map getTemperatureResult(value) {
|
||||
log.debug 'TEMP'
|
||||
def linkText = getLinkText(device)
|
||||
if (tempOffset) {
|
||||
def offset = tempOffset as int
|
||||
def v = value as int
|
||||
value = v + offset
|
||||
}
|
||||
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
|
||||
def descriptionText
|
||||
if ( temperatureScale == 'C' )
|
||||
descriptionText = '{{ device.displayName }} was {{ value }}°C'
|
||||
else
|
||||
descriptionText = '{{ device.displayName }} was {{ value }}°F'
|
||||
|
||||
return [
|
||||
name: 'temperature',
|
||||
value: value,
|
||||
descriptionText: descriptionText,
|
||||
translatable: true,
|
||||
unit: temperatureScale
|
||||
]
|
||||
}
|
||||
|
||||
private Map getMotionResult(value) {
|
||||
log.debug 'motion'
|
||||
String linkText = getLinkText(device)
|
||||
String descriptionText = value == 'active' ? "${linkText} detected motion" : "${linkText} motion has stopped"
|
||||
String descriptionText = value == 'active' ? "{{ device.displayName }} detected motion" : "{{ device.displayName }} motion has stopped"
|
||||
return [
|
||||
name: 'motion',
|
||||
value: value,
|
||||
descriptionText: descriptionText
|
||||
descriptionText: descriptionText,
|
||||
translatable: true
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "refresh called"
|
||||
def refreshCmds = [
|
||||
@@ -253,22 +279,13 @@ def refresh() {
|
||||
}
|
||||
|
||||
def configure() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
// battery minReport 30 seconds, maxReportTime 6 hrs by default
|
||||
return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
|
||||
@@ -26,10 +26,10 @@ Works with:
|
||||
|
||||
## Device Health
|
||||
|
||||
A Category C2 multi sensor with maxReportTime of 1 hr.
|
||||
Check-in interval is double the value of maxReportTime for Zigbee device.
|
||||
A Category C2 multi sensor with maxReportTime of 5 mins.
|
||||
Check-in interval is double the value of maxReportTime.
|
||||
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||
Check-in interval = 2*60 = 120 min
|
||||
Check-in interval = 12 mins
|
||||
|
||||
## Battery Specification
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
|
||||
|
||||
metadata {
|
||||
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
|
||||
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
capability "Three Axis"
|
||||
capability "Battery"
|
||||
@@ -127,13 +127,6 @@ def parse(String description) {
|
||||
map = parseIasMessage(description)
|
||||
}
|
||||
|
||||
// Temporary fix for the case when Device is OFFLINE and is connected again
|
||||
if (state.lastActivity == null){
|
||||
state.lastActivity = now()
|
||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
||||
}
|
||||
state.lastActivity = now()
|
||||
|
||||
def result = map ? createEvent(map) : null
|
||||
|
||||
if (description?.startsWith('enroll request')) {
|
||||
@@ -154,7 +147,10 @@ private Map parseCatchAllMessage(String description) {
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0001:
|
||||
// 0x07 - configure reporting
|
||||
if (cluster.command != 0x07) {
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
}
|
||||
break
|
||||
|
||||
case 0xFC02:
|
||||
@@ -162,11 +158,21 @@ private Map parseCatchAllMessage(String description) {
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
log.debug 'TEMP'
|
||||
if (cluster.command == 0x07) {
|
||||
if(cluster.data[0] == 0x00) {
|
||||
log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
else {
|
||||
log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||
}
|
||||
}
|
||||
else {
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -176,10 +182,8 @@ private Map parseCatchAllMessage(String description) {
|
||||
|
||||
private boolean shouldProcessMessage(cluster) {
|
||||
// 0x0B is default response indicating message got through
|
||||
// 0x07 is bind message
|
||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||
cluster.command == 0x0B ||
|
||||
cluster.command == 0x07 ||
|
||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||
return !ignoredMessage
|
||||
}
|
||||
@@ -268,29 +272,23 @@ def updated() {
|
||||
def getTemperature(value) {
|
||||
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
||||
if(getTemperatureScale() == "C"){
|
||||
return celsius
|
||||
return Math.round(celsius)
|
||||
} else {
|
||||
return celsiusToFahrenheit(celsius) as Integer
|
||||
return Math.round(celsiusToFahrenheit(celsius))
|
||||
}
|
||||
}
|
||||
|
||||
private Map getBatteryResult(rawValue) {
|
||||
log.debug "Battery rawValue = ${rawValue}"
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: '--',
|
||||
translatable: true
|
||||
]
|
||||
def result = [:]
|
||||
|
||||
def volts = rawValue / 10
|
||||
|
||||
if (rawValue == 0 || rawValue == 255) {}
|
||||
else {
|
||||
if (volts > 3.5) {
|
||||
result.descriptionText = "{{ device.displayName }} battery has too much power: (> 3.5) volts."
|
||||
}
|
||||
else {
|
||||
if (!(rawValue == 0 || rawValue == 255)) {
|
||||
result.name = 'battery'
|
||||
result.translatable = true
|
||||
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||
volts = rawValue // For the batteryMap to work the key needs to be an int
|
||||
def batteryMap = [28: 100, 27: 100, 26: 100, 25: 90, 24: 90, 23: 70,
|
||||
@@ -303,19 +301,15 @@ private Map getBatteryResult(rawValue) {
|
||||
else if (volts > maxVolts)
|
||||
volts = maxVolts
|
||||
def pct = batteryMap[volts]
|
||||
if (pct != null) {
|
||||
result.value = pct
|
||||
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
def roundedPct = Math.round(pct * 100)
|
||||
if (roundedPct <= 0)
|
||||
roundedPct = 1
|
||||
result.value = Math.min(100, roundedPct)
|
||||
result.descriptionText = "{{ device.displayName }} battery was {{ value }}%"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,15 +370,7 @@ private getAccelerationResult(numValue) {
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
|
||||
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
|
||||
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
|
||||
state.lastActivity = null
|
||||
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
||||
} else {
|
||||
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
|
||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
||||
}
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
@@ -414,19 +400,22 @@ def refresh() {
|
||||
}
|
||||
|
||||
def configure() {
|
||||
sendEvent(name: "checkInterval", value: 14400, displayed: false, data: [protocol: "zigbee"])
|
||||
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
|
||||
log.debug "Configuring Reporting"
|
||||
|
||||
def configCmds = enrollResponse() +
|
||||
zigbee.batteryConfig() +
|
||||
zigbee.temperatureConfig() +
|
||||
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
// battery minReport 30 seconds, maxReportTime 6 hrs by default
|
||||
def configCmds = zigbee.batteryConfig() +
|
||||
zigbee.temperatureConfig(30, 300) +
|
||||
zigbee.configureReporting(0xFC02, 0x0010, 0x18, 10, 3600, 0x01, [mfgCode: manufacturerCode]) +
|
||||
zigbee.configureReporting(0xFC02, 0x0012, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) +
|
||||
zigbee.configureReporting(0xFC02, 0x0013, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) +
|
||||
zigbee.configureReporting(0xFC02, 0x0014, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode])
|
||||
|
||||
return configCmds + refresh()
|
||||
return refresh() + configCmds
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
|
||||
@@ -28,30 +28,46 @@ import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
|
||||
capability "Sensor"
|
||||
|
||||
command "enrollResponse"
|
||||
}
|
||||
|
||||
simulator {
|
||||
|
||||
attribute "status", "string"
|
||||
}
|
||||
|
||||
preferences {
|
||||
input title: "Temperature Offset", 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"
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"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 title: "Temperature Offset", 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: "Degrees", 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?", description: "Tap to set", 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") {
|
||||
attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e"
|
||||
attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821"
|
||||
multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){
|
||||
tileAttribute ("device.status", key: "PRIMARY_CONTROL") {
|
||||
attributeState "open", label:'Open', icon:"st.contact.contact.open", backgroundColor:"#ffa81e"
|
||||
attributeState "closed", label:'Closed', 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("contact", "device.contact", width: 2, height: 2) {
|
||||
state("open", label:'Open', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
|
||||
state("closed", label:'Closed', icon:"st.contact.contact.closed", backgroundColor:"#79b821")
|
||||
}
|
||||
standardTile("acceleration", "device.acceleration", width: 2, height: 2) {
|
||||
state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0")
|
||||
state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff")
|
||||
state("active", label:'Active', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0")
|
||||
state("inactive", label:'Inactive', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff")
|
||||
}
|
||||
valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "temperature", label:'${currentValue}°',
|
||||
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||
state("temperature", label:'${currentValue}°',
|
||||
backgroundColors:[
|
||||
[value: 31, color: "#153591"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
@@ -61,6 +77,7 @@ import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
)
|
||||
}
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
@@ -69,21 +86,17 @@ import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main (["contact", "acceleration", "temperature"])
|
||||
details(["contact", "acceleration", "temperature", "battery", "refresh"])
|
||||
|
||||
main(["status", "acceleration", "temperature"])
|
||||
details(["status", "acceleration", "temperature", "battery", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
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: ')) {
|
||||
map = parseCustomMessage(description)
|
||||
}
|
||||
@@ -91,7 +104,6 @@ import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
|
||||
map = parseIasMessage(description)
|
||||
}
|
||||
|
||||
log.debug "Parse returned $map"
|
||||
def result = map ? createEvent(map) : null
|
||||
|
||||
if (description?.startsWith('enroll request')) {
|
||||
@@ -99,27 +111,45 @@ import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
|
||||
log.debug "enroll response: ${cmds}"
|
||||
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||
}
|
||||
else if (description?.startsWith('read attr -')) {
|
||||
result = parseReportAttributeMessage(description).each { createEvent(it) }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
log.debug cluster
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0001:
|
||||
// 0x07 - configure reporting
|
||||
if (cluster.command != 0x07) {
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
}
|
||||
break
|
||||
|
||||
case 0xFC02:
|
||||
log.debug 'ACCELERATION'
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
log.debug 'TEMP'
|
||||
if (cluster.command == 0x07) {
|
||||
if(cluster.data[0] == 0x00) {
|
||||
log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
else {
|
||||
log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||
}
|
||||
}
|
||||
else {
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -129,38 +159,48 @@ import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
|
||||
|
||||
private boolean shouldProcessMessage(cluster) {
|
||||
// 0x0B is default response indicating message got through
|
||||
// 0x07 is bind message
|
||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||
cluster.command == 0x0B ||
|
||||
cluster.command == 0x07 ||
|
||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||
return !ignoredMessage
|
||||
}
|
||||
|
||||
private int getHumidity(value) {
|
||||
return Math.round(Double.parseDouble(value))
|
||||
}
|
||||
|
||||
private Map parseReportAttributeMessage(String description) {
|
||||
private List parseReportAttributeMessage(String description) {
|
||||
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"
|
||||
|
||||
Map resultMap = [:]
|
||||
List result = []
|
||||
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
||||
def value = getTemperature(descMap.value)
|
||||
resultMap = getTemperatureResult(value)
|
||||
result << getTemperatureResult(value)
|
||||
}
|
||||
else if (descMap.cluster == "FC02" && descMap.attrId == "0002") {
|
||||
Integer.parseInt(descMap.value,8)
|
||||
else if (descMap.cluster == "FC02" && descMap.attrId == "0010") {
|
||||
if (descMap.value.size() == 32) {
|
||||
// value will look like 00ae29001403e2290013001629001201
|
||||
// breaking this apart and swapping byte order where appropriate, this breaks down to:
|
||||
// X (0x0012) = 0x0016
|
||||
// Y (0x0013) = 0x03E2
|
||||
// Z (0x0014) = 0x00AE
|
||||
// note that there is a known bug in that the x,y,z attributes are interpreted in the wrong order
|
||||
// this will be fixed in a future update
|
||||
def threeAxisAttributes = descMap.value[0..-9]
|
||||
result << parseAxis(threeAxisAttributes)
|
||||
descMap.value = descMap.value[-2..-1]
|
||||
}
|
||||
result << getAccelerationResult(descMap.value)
|
||||
}
|
||||
else if (descMap.cluster == "FC02" && descMap.attrId == "0012" && descMap.value.size() == 24) {
|
||||
// The size is checked to ensure the attribute report contains X, Y and Z values
|
||||
// If all three axis are not included then the attribute report is ignored
|
||||
result << parseAxis(descMap.value)
|
||||
}
|
||||
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
||||
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||
result << getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||
}
|
||||
|
||||
return resultMap
|
||||
return result
|
||||
}
|
||||
|
||||
private Map parseCustomMessage(String description) {
|
||||
@@ -174,16 +214,44 @@ private Map parseCustomMessage(String description) {
|
||||
|
||||
private Map parseIasMessage(String description) {
|
||||
ZoneStatus zs = zigbee.parseZoneStatus(description)
|
||||
Map resultMap = [:]
|
||||
|
||||
return zs.isAlarm1Set() ? getContactResult('open') : getContactResult('closed')
|
||||
if (garageSensor != "Yes"){
|
||||
resultMap = zs.isAlarm1Set() ? getContactResult('open') : getContactResult('closed')
|
||||
}
|
||||
|
||||
return resultMap
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "updated called"
|
||||
log.info "garage value : $garageSensor"
|
||||
if (garageSensor == "Yes") {
|
||||
def descriptionText = "Updating device to garage sensor"
|
||||
if (device.latestValue("status") == "open") {
|
||||
sendEvent(name: 'status', value: 'garage-open', descriptionText: descriptionText, translatable: true)
|
||||
}
|
||||
else if (device.latestValue("status") == "closed") {
|
||||
sendEvent(name: 'status', value: 'garage-closed', descriptionText: descriptionText, translatable: true)
|
||||
}
|
||||
}
|
||||
else {
|
||||
def descriptionText = "Updating device to open/close sensor"
|
||||
if (device.latestValue("status") == "garage-open") {
|
||||
sendEvent(name: 'status', value: 'open', descriptionText: descriptionText, translatable: true)
|
||||
}
|
||||
else if (device.latestValue("status") == "garage-closed") {
|
||||
sendEvent(name: 'status', value: 'closed', descriptionText: descriptionText, translatable: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getTemperature(value) {
|
||||
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
||||
if(getTemperatureScale() == "C"){
|
||||
return celsius
|
||||
return Math.round(celsius)
|
||||
} else {
|
||||
return celsiusToFahrenheit(celsius) as Integer
|
||||
return Math.round(celsiusToFahrenheit(celsius))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,109 +259,130 @@ def getTemperature(value) {
|
||||
log.debug 'Battery'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
def result = [
|
||||
name: 'battery'
|
||||
]
|
||||
def result = [:]
|
||||
|
||||
def volts = rawValue / 10
|
||||
def descriptionText
|
||||
if (rawValue == 0 || rawValue == 255) {}
|
||||
else if (volts > 3.5) {
|
||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||
}
|
||||
else {
|
||||
|
||||
if (!(rawValue == 0 || rawValue == 255)) {
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
def roundedPct = Math.round(pct * 100)
|
||||
if (roundedPct <= 0)
|
||||
roundedPct = 1
|
||||
result.name = 'battery'
|
||||
result.value = Math.min(100, roundedPct)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private Map getTemperatureResult(value) {
|
||||
log.debug 'TEMP'
|
||||
def linkText = getLinkText(device)
|
||||
log.debug "Temperature"
|
||||
if (tempOffset) {
|
||||
def offset = tempOffset as int
|
||||
def v = value as int
|
||||
value = v + offset
|
||||
}
|
||||
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
|
||||
def descriptionText = temperatureScale == 'C' ? '{{ device.displayName }} was {{ value }}°C':
|
||||
'{{ device.displayName }} was {{ value }}°F'
|
||||
|
||||
return [
|
||||
name: 'temperature',
|
||||
value: value,
|
||||
descriptionText: descriptionText,
|
||||
translatable: true,
|
||||
unit: temperatureScale
|
||||
]
|
||||
}
|
||||
|
||||
private Map getContactResult(value) {
|
||||
log.debug 'Contact Status'
|
||||
def linkText = getLinkText(device)
|
||||
def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}"
|
||||
return [
|
||||
name: 'contact',
|
||||
value: value,
|
||||
descriptionText: descriptionText
|
||||
]
|
||||
log.debug "Contact: ${device.displayName} value = ${value}"
|
||||
def descriptionText = value == 'open' ? '{{ device.displayName }} was opened' : '{{ device.displayName }} was closed'
|
||||
sendEvent(name: 'contact', value: value, descriptionText: descriptionText, displayed: false, translatable: true)
|
||||
sendEvent(name: 'status', value: value, descriptionText: descriptionText, translatable: true)
|
||||
}
|
||||
|
||||
private getAccelerationResult(numValue) {
|
||||
log.debug "Acceleration"
|
||||
def name = "acceleration"
|
||||
def value = numValue.endsWith("1") ? "active" : "inactive"
|
||||
//def linkText = getLinkText(device)
|
||||
def descriptionText = "$linkText was $value"
|
||||
def value
|
||||
def descriptionText
|
||||
|
||||
if ( numValue.endsWith("1") ) {
|
||||
value = "active"
|
||||
descriptionText = '{{ device.displayName }} was active'
|
||||
} else {
|
||||
value = "inactive"
|
||||
descriptionText = '{{ device.displayName }} was inactive'
|
||||
}
|
||||
|
||||
def isStateChange = isStateChange(device, name, value)
|
||||
[
|
||||
return [
|
||||
name: name,
|
||||
value: value,
|
||||
//unit: null,
|
||||
//linkText: linkText,
|
||||
descriptionText: descriptionText,
|
||||
//handlerName: value,
|
||||
isStateChange: isStateChange
|
||||
// displayed: displayed(description, isStateChange)
|
||||
isStateChange: isStateChange,
|
||||
translatable: true
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Refreshing Temperature and Battery "
|
||||
def refreshCmds = [
|
||||
log.debug "Refreshing Values "
|
||||
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||
//"st rattr 0x${device.deviceNetworkId} 1 0xFC02 2", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200"
|
||||
def refreshCmds = []
|
||||
|
||||
]
|
||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||
log.debug "Refreshing Values for manufacturer: SmartThings "
|
||||
/* These values of Motion Threshold Multiplier(0x01) and Motion Threshold (0x0276)
|
||||
seem to be giving pretty accurate results for the XYZ co-ordinates for this manufacturer.
|
||||
Separating these out in a separate if-else because I do not want to touch Centralite part
|
||||
as of now.
|
||||
*/
|
||||
refreshCmds += zigbee.writeAttribute(0xFC02, 0x0000, 0x20, 0x01, [mfgCode: manufacturerCode])
|
||||
refreshCmds += zigbee.writeAttribute(0xFC02, 0x0002, 0x21, 0x0276, [mfgCode: manufacturerCode])
|
||||
} else {
|
||||
refreshCmds += zigbee.writeAttribute(0xFC02, 0x0000, 0x20, 0x02, [mfgCode: manufacturerCode])
|
||||
}
|
||||
|
||||
//Common refresh commands
|
||||
refreshCmds += zigbee.readAttribute(0x0402, 0x0000) +
|
||||
zigbee.readAttribute(0x0001, 0x0020) +
|
||||
zigbee.readAttribute(0xFC02, 0x0010, [mfgCode: manufacturerCode])
|
||||
|
||||
return refreshCmds + enrollResponse()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
sendEvent(name: "checkInterval", value: 7200, displayed: false)
|
||||
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
log.debug "Configuring Reporting"
|
||||
|
||||
"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",
|
||||
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
// battery minReport 30 seconds, maxReportTime 6 hrs by default
|
||||
def configCmds = zigbee.batteryConfig() +
|
||||
zigbee.temperatureConfig(30, 300) +
|
||||
zigbee.configureReporting(0xFC02, 0x0010, 0x18, 10, 3600, 0x01, [mfgCode: manufacturerCode]) +
|
||||
zigbee.configureReporting(0xFC02, 0x0012, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) +
|
||||
zigbee.configureReporting(0xFC02, 0x0013, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) +
|
||||
zigbee.configureReporting(0xFC02, 0x0014, 0x29, 1, 3600, 0x0001, [mfgCode: manufacturerCode])
|
||||
|
||||
"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",
|
||||
return refresh() + configCmds
|
||||
}
|
||||
|
||||
"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
|
||||
private getEndpointId() {
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
@@ -304,13 +393,91 @@ def enrollResponse() {
|
||||
"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}",
|
||||
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
|
||||
]
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
private Map parseAxis(String description) {
|
||||
def z = hexToSignedInt(description[0..3])
|
||||
def y = hexToSignedInt(description[10..13])
|
||||
def x = hexToSignedInt(description[20..23])
|
||||
def xyzResults = [x: x, y: y, z: z]
|
||||
|
||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||
// This mapping matches the current behavior of the Device Handler for the Centralite sensors
|
||||
xyzResults.x = z
|
||||
xyzResults.y = y
|
||||
xyzResults.z = -x
|
||||
} else {
|
||||
// The axises reported by the Device Handler differ from the axises reported by the sensor
|
||||
// This may change in the future
|
||||
xyzResults.x = z
|
||||
xyzResults.y = x
|
||||
xyzResults.z = y
|
||||
}
|
||||
|
||||
log.debug "parseAxis -- ${xyzResults}"
|
||||
|
||||
if (garageSensor == "Yes")
|
||||
garageEvent(xyzResults.z)
|
||||
|
||||
getXyzResult(xyzResults, description)
|
||||
}
|
||||
|
||||
private hexToSignedInt(hexVal) {
|
||||
def unsignedVal = hexToInt(hexVal)
|
||||
unsignedVal > 32767 ? unsignedVal - 65536 : unsignedVal
|
||||
}
|
||||
|
||||
def garageEvent(zValue) {
|
||||
def absValue = zValue.abs()
|
||||
def contactValue = null
|
||||
def garageValue = null
|
||||
if (absValue>900) {
|
||||
contactValue = 'closed'
|
||||
garageValue = 'garage-closed'
|
||||
}
|
||||
else if (absValue < 100) {
|
||||
contactValue = 'open'
|
||||
garageValue = 'garage-open'
|
||||
}
|
||||
if (contactValue != null){
|
||||
def descriptionText = contactValue == 'open' ? '{{ device.displayName }} was opened' :'{{ device.displayName }} was closed'
|
||||
sendEvent(name: 'contact', value: contactValue, descriptionText: descriptionText, displayed:false, translatable: true)
|
||||
sendEvent(name: 'status', value: garageValue, descriptionText: descriptionText, translatable: true)
|
||||
}
|
||||
}
|
||||
|
||||
private Map getXyzResult(results, description) {
|
||||
def name = "threeAxis"
|
||||
def value = "${results.x},${results.y},${results.z}"
|
||||
def linkText = getLinkText(device)
|
||||
def descriptionText = "$linkText was $value"
|
||||
def isStateChange = isStateChange(device, name, value)
|
||||
|
||||
[
|
||||
name: name,
|
||||
value: value,
|
||||
unit: null,
|
||||
linkText: linkText,
|
||||
descriptionText: descriptionText,
|
||||
handlerName: name,
|
||||
isStateChange: isStateChange,
|
||||
displayed: false
|
||||
]
|
||||
}
|
||||
|
||||
private getManufacturerCode() {
|
||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||
return "0x110A"
|
||||
} else {
|
||||
return "0x104E"
|
||||
}
|
||||
}
|
||||
|
||||
private hexToInt(value) {
|
||||
new BigInteger(value, 16)
|
||||
}
|
||||
|
||||
private hex(value) {
|
||||
|
||||
@@ -24,10 +24,10 @@ Works with:
|
||||
|
||||
## Device Health
|
||||
|
||||
A Category C2 open/closed sensor with maxReportTime of 1 hr.
|
||||
Check-in interval is double the value of maxReportTime for Zigbee device.
|
||||
A Category C2 open/closed sensor with maxReportTime of 5 mins.
|
||||
Check-in interval is double the value of maxReportTime.
|
||||
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||
Check-in interval = 2*60 = 120 min
|
||||
Check-in interval = 12 mins
|
||||
|
||||
## Battery Specification
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import physicalgraph.zigbee.clusters.iaszone.ZoneStatus
|
||||
|
||||
metadata {
|
||||
definition (name: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings", category: "C2") {
|
||||
definition (name: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Battery"
|
||||
capability "Configuration"
|
||||
capability "Contact Sensor"
|
||||
@@ -92,13 +92,6 @@ def parse(String description) {
|
||||
map = parseIasMessage(description)
|
||||
}
|
||||
|
||||
// Temporary fix for the case when Device is OFFLINE and is connected again
|
||||
if (state.lastActivity == null){
|
||||
state.lastActivity = now()
|
||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
||||
}
|
||||
state.lastActivity = now()
|
||||
|
||||
log.debug "Parse returned $map"
|
||||
def result = map ? createEvent(map) : null
|
||||
|
||||
@@ -116,15 +109,28 @@ private Map parseCatchAllMessage(String description) {
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0001:
|
||||
// 0x07 - configure reporting
|
||||
if (cluster.command != 0x07) {
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
}
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
log.debug 'TEMP'
|
||||
if (cluster.command == 0x07){
|
||||
if (cluster.data[0] == 0x00) {
|
||||
log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
else {
|
||||
log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||
}
|
||||
}
|
||||
else {
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -134,10 +140,8 @@ private Map parseCatchAllMessage(String description) {
|
||||
|
||||
private boolean shouldProcessMessage(cluster) {
|
||||
// 0x0B is default response indicating message got through
|
||||
// 0x07 is bind message
|
||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||
cluster.command == 0x0B ||
|
||||
cluster.command == 0x07 ||
|
||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||
return !ignoredMessage
|
||||
}
|
||||
@@ -192,23 +196,19 @@ private Map getBatteryResult(rawValue) {
|
||||
log.debug 'Battery'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
def result = [
|
||||
name: 'battery'
|
||||
]
|
||||
def result = [:]
|
||||
|
||||
def volts = rawValue / 10
|
||||
def descriptionText
|
||||
if (rawValue == 0 || rawValue == 255) {}
|
||||
else if (volts > 3.5) {
|
||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||
}
|
||||
else {
|
||||
if (!(rawValue == 0 || rawValue == 255)) {
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
def roundedPct = Math.round(pct * 100)
|
||||
if (roundedPct <= 0)
|
||||
roundedPct = 1
|
||||
result.value = Math.min(100, roundedPct)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
result.name = 'battery'
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -246,15 +246,7 @@ private Map getContactResult(value) {
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
|
||||
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
|
||||
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
|
||||
state.lastActivity = null
|
||||
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
||||
} else {
|
||||
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
|
||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
||||
}
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
@@ -268,23 +260,15 @@ def refresh() {
|
||||
}
|
||||
|
||||
def configure() {
|
||||
sendEvent(name: "checkInterval", value: 14400, displayed: false, data: [protocol: "zigbee"])
|
||||
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
// battery minReport 30 seconds, maxReportTime 6 hrs by default
|
||||
return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
|
||||
@@ -24,10 +24,10 @@ Works with:
|
||||
|
||||
## Device Health
|
||||
|
||||
A Category C2 SmartSense Temp/Humidity Sensor with maxReportTime of 1 hr.
|
||||
Check-in interval is double the value of maxReportTime for Zigbee device.
|
||||
A Category C2 SmartSense Temp/Humidity Sensor with maxReportTime of 5 mins.
|
||||
Check-in interval is double the value of maxReportTime.
|
||||
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||
Check-in interval = 2*60 = 120 min
|
||||
Check-in interval = 12 mins
|
||||
|
||||
## Battery Specification
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "SmartSense Temp/Humidity Sensor",namespace: "smartthings", author: "SmartThings", category: "C2") {
|
||||
definition (name: "SmartSense Temp/Humidity Sensor",namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Configuration"
|
||||
capability "Battery"
|
||||
capability "Refresh"
|
||||
@@ -83,13 +83,6 @@ def parse(String description) {
|
||||
map = parseCustomMessage(description)
|
||||
}
|
||||
|
||||
// Temporary fix for the case when Device is OFFLINE and is connected again
|
||||
if (state.lastActivity == null){
|
||||
state.lastActivity = now()
|
||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
||||
}
|
||||
state.lastActivity = now()
|
||||
|
||||
log.debug "Parse returned $map"
|
||||
return map ? createEvent(map) : null
|
||||
}
|
||||
@@ -100,20 +93,37 @@ private Map parseCatchAllMessage(String description) {
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0001:
|
||||
// 0x07 - configure reporting
|
||||
if (cluster.command != 0x07) {
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
}
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
if (cluster.command == 0x07) {
|
||||
if (cluster.data[0] == 0x00){
|
||||
log.debug "TEMP REPORTING CONFIG RESPONSE" + cluster
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
else {
|
||||
log.warn "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||
}
|
||||
}
|
||||
else {
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
}
|
||||
break
|
||||
|
||||
case 0xFC45:
|
||||
// 0x07 - configure reporting
|
||||
if (cluster.command != 0x07) {
|
||||
String pctStr = cluster.data[-1, -2].collect { Integer.toHexString(it) }.join('')
|
||||
String display = Math.round(Integer.valueOf(pctStr, 16) / 100)
|
||||
resultMap = getHumidityResult(display)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -123,10 +133,8 @@ private Map parseCatchAllMessage(String description) {
|
||||
|
||||
private boolean shouldProcessMessage(cluster) {
|
||||
// 0x0B is default response indicating message got through
|
||||
// 0x07 is bind message
|
||||
boolean ignoredMessage = cluster.profileId != 0x0104 ||
|
||||
cluster.command == 0x0B ||
|
||||
cluster.command == 0x07 ||
|
||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)
|
||||
return !ignoredMessage
|
||||
}
|
||||
@@ -199,23 +207,20 @@ private Map getBatteryResult(rawValue) {
|
||||
log.debug 'Battery'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
def result = [
|
||||
name: 'battery'
|
||||
]
|
||||
def result = [:]
|
||||
|
||||
def volts = rawValue / 10
|
||||
def descriptionText
|
||||
if (rawValue == 0 || rawValue == 255) {}
|
||||
else if (volts > 3.5) {
|
||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||
}
|
||||
else {
|
||||
if (!(rawValue == 0 || rawValue == 255)) {
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
def roundedPct = Math.round(pct * 100)
|
||||
if (roundedPct <= 0)
|
||||
roundedPct = 1
|
||||
result.value = Math.min(100, roundedPct)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
result.name = 'battery'
|
||||
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -240,59 +245,39 @@ private Map getTemperatureResult(value) {
|
||||
|
||||
private Map getHumidityResult(value) {
|
||||
log.debug 'Humidity'
|
||||
return [
|
||||
name: 'humidity',
|
||||
value: value,
|
||||
unit: '%'
|
||||
]
|
||||
return value ? [name: 'humidity', value: value, unit: '%'] : [:]
|
||||
}
|
||||
|
||||
/**
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
|
||||
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
|
||||
state.lastActivity = null
|
||||
return zigbee.readAttribute(0x001, 0x0020) // Read the Battery Level
|
||||
} else {
|
||||
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
|
||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
||||
}
|
||||
}
|
||||
|
||||
def refresh()
|
||||
{
|
||||
log.debug "refresh temperature, humidity, and battery"
|
||||
[
|
||||
|
||||
"zcl mfg-code 0xC2DF", "delay 1000",
|
||||
"zcl global read 0xFC45 0", "delay 1000",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1000",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20"
|
||||
|
||||
]
|
||||
return zigbee.readAttribute(0xFC45, 0x0000, ["mfgCode": 0xC2DF]) + // Original firmware
|
||||
zigbee.readAttribute(0x0402, 0x0000) +
|
||||
zigbee.readAttribute(0x0001, 0x0020)
|
||||
}
|
||||
|
||||
def configure() {
|
||||
sendEvent(name: "checkInterval", value: 14400, displayed: false, data: [protocol: "zigbee"])
|
||||
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
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",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
def humidityConfigCmds = [
|
||||
"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
|
||||
|
||||
// temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
// battery minReport 30 seconds, maxReportTime 6 hrs by default
|
||||
return refresh() + humidityConfigCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
private hex(value) {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.st-ignore
|
||||
README.md
|
||||
@@ -0,0 +1,42 @@
|
||||
# Tyco Door Window Sensor
|
||||
|
||||
|
||||
|
||||
Works with:
|
||||
|
||||
* [Tyco Door Window Sensor](https://support.smartthings.com/hc/en-us/articles/204834100-Tyco-Door-Window-Sensor)
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Capabilities](#capabilities)
|
||||
* [Health](#device-health)
|
||||
* [Battery](#battery-specification)
|
||||
|
||||
## Capabilities
|
||||
|
||||
* **Battery** - defines device uses a battery
|
||||
* **Configuration** - _configure()_ command called when device is installed or device preferences updated
|
||||
* **Contact Sensor** - can detect contact (open/close)
|
||||
* **Refresh** - _refresh()_ command for status updates
|
||||
* **Temperature Measurement** - can measure the device temperature
|
||||
* **Health Check** - indicates ability to get device health notifications
|
||||
|
||||
## Device Health
|
||||
|
||||
Contact sensor with maxReportTime of 5 mins.
|
||||
Check-in interval is double the value of maxReportTime for Zigbee device.
|
||||
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||
Check-in interval = 12 min
|
||||
|
||||
## Battery Specification
|
||||
|
||||
3V CR2032 battery is required.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that either the sensor needs to be reseted or the sensor is out of range.
|
||||
Reset needs to be done by inserting the battery in the sensor and then quickly pressing the adjacent black button 10 times. Pairing should be tried again now.
|
||||
It may happen that sensor is out of range, then pairing needs to be tried again by placing the sensor closer to the hub.
|
||||
Instructions related to pairing, resetting and removing the different motion sensors from SmartThings can be found in the following links
|
||||
for the different models:
|
||||
* [Tyco Door Window Sensor (MCT-340)](https://support.smartthings.com/hc/en-us/articles/204834100-Tyco-Door-Window-Sensor)
|
||||
@@ -22,6 +22,7 @@ metadata {
|
||||
capability "Contact Sensor"
|
||||
capability "Refresh"
|
||||
capability "Temperature Measurement"
|
||||
capability "Health Check"
|
||||
|
||||
command "enrollResponse"
|
||||
|
||||
@@ -180,22 +181,17 @@ private Map getBatteryResult(rawValue) {
|
||||
log.debug 'Battery'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
def result = [
|
||||
name: 'battery'
|
||||
]
|
||||
def result = [:]
|
||||
|
||||
if (!(rawValue == 0 || rawValue == 255)) {
|
||||
def volts = rawValue / 10
|
||||
def descriptionText
|
||||
if (volts > 3.5) {
|
||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||
}
|
||||
else {
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
def roundedPct = Math.round(pct * 100)
|
||||
result.value = Math.min(100, roundedPct)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
result.name = 'battery'
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -229,44 +225,42 @@ private Map getContactResult(value) {
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
return zigbee.readAttribute(0x0402, 0x0000) // Read the Temperature Cluster
|
||||
}
|
||||
|
||||
def refresh()
|
||||
{
|
||||
log.debug "Refreshing Temperature and Battery"
|
||||
[
|
||||
def refreshCmds = [
|
||||
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20"
|
||||
|
||||
]
|
||||
|
||||
return refreshCmds + enrollResponse()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
// Device-Watch allows 2 check-in misses from device
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
def enrollCmds = [
|
||||
"delay 1000",
|
||||
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
|
||||
//"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||
//"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}",
|
||||
|
||||
"delay 500"
|
||||
]
|
||||
return configCmds + enrollResponse() + refresh() // send refresh cmds as part of config
|
||||
return enrollCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
|
||||
@@ -89,14 +89,8 @@ def parse(String description) {
|
||||
}
|
||||
|
||||
private Map parseIasButtonMessage(String description) {
|
||||
int zoneInt = Integer.parseInt((description - "zone status 0x"), 16)
|
||||
if (zoneInt & 0x02) {
|
||||
resultMap = getButtonResult('press')
|
||||
} else {
|
||||
resultMap = getButtonResult('release')
|
||||
}
|
||||
|
||||
return resultMap
|
||||
def zs = zigbee.parseZoneStatus(description)
|
||||
return zs.isAlarm2Set() ? getButtonResult("press") : getButtonResult("release")
|
||||
}
|
||||
|
||||
private Map getBatteryResult(rawValue) {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.st-ignore
|
||||
README.md
|
||||
41
devicetypes/smartthings/zigbee-dimmer-power.src/README.md
Normal file
41
devicetypes/smartthings/zigbee-dimmer-power.src/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# GE Plug-In/In-Wall Smart Dimmer (ZigBee)
|
||||
|
||||
|
||||
|
||||
Works with:
|
||||
|
||||
* [GE In-Wall Smart Dimmer (ZigBee)](https://shop.smartthings.com/#!/products/ge-in-wall-smart-dimmer-switch)
|
||||
* [GE Plug-In Smart Dimmer (ZigBee)](https://www.smartthings.com/works-with-smartthings/ge/ge-plug-in-smart-dimmer-zigbee)
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Capabilities](#capabilities)
|
||||
* [Health](#device-health)
|
||||
* [Troubleshooting](#Troubleshooting)
|
||||
|
||||
## Capabilities
|
||||
|
||||
* **Actuator** - represents that a Device has commands
|
||||
* **Configuration** - _configure()_ command called when device is installed or device preferences updated
|
||||
* **Refresh** - _refresh()_ command for status updates
|
||||
* **Power Meter** - ability to check the power meter(energy consumption) of device
|
||||
* **Sensor** - represents the device sensor capability
|
||||
* **Switch** - can detect state (possible values: on/off)
|
||||
* **Switch Level** - represents current light level, usually 0-100 in percent
|
||||
* **Health Check** - indicates ability to get device health notifications
|
||||
|
||||
## Device Health
|
||||
|
||||
A Zigbee dimmer with maxReportTime of 5 mins.
|
||||
Check-in interval is double the value of maxReportTime.
|
||||
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||
Enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||
It then enrolls the device with updated checkInterval i.e. 12 mins
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range.
|
||||
Pairing needs to be tried again by placing the device closer to the hub.
|
||||
Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link:
|
||||
* [GE Z-Wave In-Wall Smart Dimmer (GE 45857) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/204988564-GE-In-Wall-Smart-Dimmer-45857GE-ZigBee-)
|
||||
* [GE Zigbee Plug-in Smart Dimmer (GE 45852) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/205239280-GE-Plug-In-Smart-Dimmer-45852GE-ZigBee-)
|
||||
@@ -21,6 +21,7 @@ metadata {
|
||||
capability "Sensor"
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
capability "Health Check"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702"
|
||||
@@ -69,11 +70,23 @@ def parse(String description) {
|
||||
sendEvent(event)
|
||||
}
|
||||
}
|
||||
else {
|
||||
def cluster = zigbee.parse(description)
|
||||
if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) {
|
||||
if (cluster.data[0] == 0x00){
|
||||
log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
else {
|
||||
log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug zigbee.parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
zigbee.off()
|
||||
@@ -87,11 +100,22 @@ def setLevel(value) {
|
||||
zigbee.setLevel(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
return zigbee.onOffRefresh()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig()
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig(0, 300) + 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()
|
||||
|
||||
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||
sendEvent(name: "checkInterval", value: 3 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
refresh()
|
||||
}
|
||||
|
||||
2
devicetypes/smartthings/zigbee-dimmer.src/.st-ignore
Normal file
2
devicetypes/smartthings/zigbee-dimmer.src/.st-ignore
Normal file
@@ -0,0 +1,2 @@
|
||||
.st-ignore
|
||||
README.md
|
||||
36
devicetypes/smartthings/zigbee-dimmer.src/README.md
Normal file
36
devicetypes/smartthings/zigbee-dimmer.src/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# OSRAM Lightify LED On/Off/Dim
|
||||
|
||||
|
||||
|
||||
Works with:
|
||||
|
||||
* [OSRAM Lightify LED On/Off/Dim](https://shop.smartthings.com/#!/products/osram-led-smart-bulb-on-off-dim)
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Capabilities](#capabilities)
|
||||
* [Health](#device-health)
|
||||
* [Battery](#battery-specification)
|
||||
|
||||
## Capabilities
|
||||
|
||||
* **Actuator** - represents that a Device has commands
|
||||
* **Configuration** - _configure()_ command called when device is installed or device preferences updated
|
||||
* **Refresh** - _refresh()_ command for status updates
|
||||
* **Switch** - can detect state (possible values: on/off)
|
||||
* **Switch Level** - represents current light level, usually 0-100 in percent
|
||||
* **Health Check** - indicates ability to get device health notifications
|
||||
|
||||
## Device Health
|
||||
|
||||
A Category C1 Zigbee dimmer with maxReportTime of 5 mins.
|
||||
Check-in interval is double the value of maxReportTime.
|
||||
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||
Check-in interval = 12 mins
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range.
|
||||
Pairing needs to be tried again by placing the device closer to the hub.
|
||||
Other troubleshooting tips are listed as follows:
|
||||
* [Troubleshooting:](https://support.smartthings.com/hc/en-us/articles/207191763-OSRAM-LIGHTIFY-LED-Smart-Connected-Light-A19-On-Off-Dim)
|
||||
@@ -54,20 +54,27 @@ def parse(String description) {
|
||||
|
||||
def event = zigbee.getEvent(description)
|
||||
if (event) {
|
||||
// Temporary fix for the case when Device is OFFLINE and is connected again
|
||||
if (state.lastActivity == null){
|
||||
state.lastActivity = now()
|
||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
||||
}
|
||||
state.lastActivity = now()
|
||||
if (event.name=="level" && event.value==0) {}
|
||||
else {
|
||||
sendEvent(event)
|
||||
}
|
||||
}
|
||||
else {
|
||||
def cluster = zigbee.parse(description)
|
||||
|
||||
if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) {
|
||||
if (cluster.data[0] == 0x00) {
|
||||
log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
else {
|
||||
log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug zigbee.parseDescriptionAsMap(description)
|
||||
log.debug "${cluster}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,24 +93,19 @@ def setLevel(value) {
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
|
||||
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
|
||||
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
|
||||
state.lastActivity = null
|
||||
return zigbee.onOffRefresh()
|
||||
} else {
|
||||
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
|
||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
||||
}
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig()
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig(0, 300) + zigbee.levelConfig()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
// Enrolls device to Device-Watch with 3 x Reporting interval 30min
|
||||
sendEvent(name: "checkInterval", value: 1800, displayed: false, data: [protocol: "zigbee"])
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||
sendEvent(name: "checkInterval", value: 3 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
|
||||
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig(0, 300) + zigbee.levelConfig()
|
||||
}
|
||||
|
||||
@@ -47,197 +47,122 @@ metadata {
|
||||
//fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", 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: 1, height: 1, canChangeIcon: true) {
|
||||
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") {
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel"
|
||||
}
|
||||
tileAttribute ("device.color", key: "COLOR_CONTROL") {
|
||||
attributeState "color", action:"color control.setColor"
|
||||
}
|
||||
}
|
||||
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'
|
||||
}
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) {
|
||||
state "color", action:"setAdjustedColor"
|
||||
}
|
||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||
state "level", label: 'Level ${currentValue}%'
|
||||
}
|
||||
controlTile("saturationSliderControl", "device.saturation", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||
state "saturation", action:"color control.setSaturation"
|
||||
}
|
||||
valueTile("saturation", "device.saturation", inactiveLabel: false, decoration: "flat") {
|
||||
state "saturation", label: 'Sat ${currentValue} '
|
||||
}
|
||||
controlTile("hueSliderControl", "device.hue", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||
state "hue", action:"color control.setHue"
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "levelSliderControl", "rgbSelector", "refresh"])
|
||||
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
//Globals
|
||||
private getATTRIBUTE_HUE() { 0x0000 }
|
||||
private getATTRIBUTE_SATURATION() { 0x0001 }
|
||||
private getHUE_COMMAND() { 0x00 }
|
||||
private getSATURATION_COMMAND() { 0x03 }
|
||||
private getCOLOR_CONTROL_CLUSTER() { 0x0300 }
|
||||
private getATTRIBUTE_COLOR_TEMPERATURE() { 0x0007 }
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
//log.trace description
|
||||
if (description?.startsWith("catchall:")) {
|
||||
def msg = zigbee.parse(description)
|
||||
//log.trace msg
|
||||
//log.trace "data: $msg.data"
|
||||
log.debug "description is $description"
|
||||
|
||||
def finalResult = zigbee.getEvent(description)
|
||||
if (finalResult) {
|
||||
log.debug finalResult
|
||||
sendEvent(finalResult)
|
||||
}
|
||||
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
|
||||
def zigbeeMap = zigbee.parseDescriptionAsMap(description)
|
||||
log.trace "zigbeeMap : $zigbeeMap"
|
||||
|
||||
if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) {
|
||||
if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute
|
||||
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 360)
|
||||
sendEvent(name: "hue", value: hueValue, displayed:false)
|
||||
}
|
||||
else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute
|
||||
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||
sendEvent(name: "saturation", value: saturationValue, displayed:false)
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.info "DID NOT PARSE MESSAGE for description : $description"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def on() {
|
||||
// just assume it works for now
|
||||
log.debug "on()"
|
||||
sendEvent(name: "switch", value: "on")
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}"
|
||||
zigbee.on() + ["delay 1500"] + zigbee.onOffRefresh()
|
||||
}
|
||||
|
||||
def off() {
|
||||
// just assume it works for now
|
||||
log.debug "off()"
|
||||
sendEvent(name: "switch", value: "off")
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
|
||||
zigbee.off() + ["delay 1500"] + zigbee.onOffRefresh()
|
||||
}
|
||||
|
||||
def setHue(value) {
|
||||
def max = 0xfe
|
||||
log.trace "setHue($value)"
|
||||
sendEvent(name: "hue", value: value)
|
||||
def scaledValue = Math.round(value * max / 100.0)
|
||||
def cmd = "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x00 {${hex(scaledValue)} 00 0000}"
|
||||
//log.info cmd
|
||||
cmd
|
||||
def refresh() {
|
||||
refreshAttributes() + configureAttributes()
|
||||
}
|
||||
|
||||
def setAdjustedColor(value) {
|
||||
log.debug "setAdjustedColor: ${value}"
|
||||
def adjusted = value + [:]
|
||||
adjusted.hue = adjustOutgoingHue(value.hue)
|
||||
adjusted.level = null // needed because color picker always sends 100
|
||||
setColor(adjusted)
|
||||
def poll() {
|
||||
refreshAttributes()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
configureAttributes() + refreshAttributes()
|
||||
}
|
||||
|
||||
def configureAttributes() {
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01)
|
||||
}
|
||||
|
||||
def refreshAttributes() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
}
|
||||
|
||||
def setColorTemperature(value) {
|
||||
zigbee.setColorTemperature(value) + ["delay 1500"] + zigbee.colorTemperatureRefresh()
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
zigbee.setLevel(value) + ["delay 1500"] + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report
|
||||
}
|
||||
|
||||
def setColor(value){
|
||||
log.trace "setColor($value)"
|
||||
def max = 0xfe
|
||||
|
||||
sendEvent(name: "hue", value: value.hue)
|
||||
sendEvent(name: "saturation", value: value.saturation)
|
||||
def scaledHueValue = Math.round(value.hue * max / 100.0)
|
||||
def scaledSatValue = Math.round(value.saturation * max / 100.0)
|
||||
|
||||
def cmd = []
|
||||
if (value.switch != "off" && device.latestValue("switch") == "off") {
|
||||
cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}"
|
||||
cmd << "delay 150"
|
||||
zigbee.on() + setHue(value.hue) + ["delay 300"] + setSaturation(value.saturation) + ["delay 2000"] + refreshAttributes()
|
||||
}
|
||||
|
||||
cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x00 {${hex(scaledHueValue)} 00 0000}"
|
||||
cmd << "delay 150"
|
||||
cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x03 {${hex(scaledSatValue)} 0000}"
|
||||
|
||||
if (value.level != null) {
|
||||
cmd << "delay 150"
|
||||
cmd.addAll(setLevel(value.level))
|
||||
}
|
||||
|
||||
if (value.switch == "off") {
|
||||
cmd << "delay 150"
|
||||
cmd << off()
|
||||
}
|
||||
log.info cmd
|
||||
cmd
|
||||
def setHue(value) {
|
||||
def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5)
|
||||
}
|
||||
|
||||
def setSaturation(value) {
|
||||
def max = 0xfe
|
||||
log.trace "setSaturation($value)"
|
||||
sendEvent(name: "saturation", value: value)
|
||||
def scaledValue = Math.round(value * max / 100.0)
|
||||
def cmd = "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x03 {${hex(scaledValue)} 0000}"
|
||||
//log.info cmd
|
||||
cmd
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
"st rattr 0x${device.deviceNetworkId} 1 6 0"
|
||||
}
|
||||
|
||||
def poll(){
|
||||
log.debug "Poll is calling refresh"
|
||||
refresh()
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
|
||||
private hex(value, width=2) {
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() < width) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
private adjustOutgoingHue(percent) {
|
||||
def adjusted = percent
|
||||
if (percent > 31) {
|
||||
if (percent < 63.0) {
|
||||
adjusted = percent + (7 * (percent -30 ) / 32)
|
||||
}
|
||||
else if (percent < 73.0) {
|
||||
adjusted = 69 + (5 * (percent - 62) / 10)
|
||||
}
|
||||
else {
|
||||
adjusted = percent + (2 * (100 - percent) / 28)
|
||||
}
|
||||
}
|
||||
log.info "percent: $percent, adjusted: $adjusted"
|
||||
adjusted
|
||||
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) //payload-> sat value, transition time
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD210 PB DB", deviceJoinName: "Yale Push Button Deadbolt Lock"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD220/240 TSDB", deviceJoinName: "Yale Touch Screen Deadbolt Lock"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRL210 PB LL", deviceJoinName: "Yale Push Button Lever Lock"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD226/246 TSDB", deviceJoinName: "Yale Touch Screen Deadbolt Lock"
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
@@ -89,7 +90,7 @@ def configure() {
|
||||
zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING,
|
||||
TYPE_U8, 600, 21600, 0x01)
|
||||
log.info "configure() --- cmds: $cmds"
|
||||
return cmds + refresh() // send refresh cmds as part of config
|
||||
return refresh() + cmds // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2016-01-19
|
||||
*
|
||||
* This DTH should serve as the generic DTH to handle RGB ZigBee HA devices (For color bulbs with no color temperature)
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "ZigBee RGB Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
capability "Actuator"
|
||||
capability "Color Control"
|
||||
capability "Configuration"
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
capability "Health Check"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenspot RGB", deviceJoinName: "OSRAM LIGHTIFY Gardenspot mini RGB"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Gardenspot RGB", deviceJoinName: "OSRAM LIGHTIFY Gardenspot mini RGB"
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel"
|
||||
}
|
||||
tileAttribute ("device.color", key: "COLOR_CONTROL") {
|
||||
attributeState "color", action:"color control.setColor"
|
||||
}
|
||||
}
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
//Globals
|
||||
private getATTRIBUTE_HUE() { 0x0000 }
|
||||
private getATTRIBUTE_SATURATION() { 0x0001 }
|
||||
private getHUE_COMMAND() { 0x00 }
|
||||
private getSATURATION_COMMAND() { 0x03 }
|
||||
private getCOLOR_CONTROL_CLUSTER() { 0x0300 }
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def event = zigbee.getEvent(description)
|
||||
if (event) {
|
||||
log.debug event
|
||||
if (event.name=="level" && event.value==0) {}
|
||||
else {
|
||||
sendEvent(event)
|
||||
}
|
||||
}
|
||||
else {
|
||||
def zigbeeMap = zigbee.parseDescriptionAsMap(description)
|
||||
def cluster = zigbee.parse(description)
|
||||
|
||||
if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) {
|
||||
if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute
|
||||
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||
sendEvent(name: "hue", value: hueValue, descriptionText: "Color has changed")
|
||||
}
|
||||
else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute
|
||||
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||
sendEvent(name: "saturation", value: saturationValue, descriptionText: "Color has changed", displayed: false)
|
||||
}
|
||||
}
|
||||
else if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) {
|
||||
if (cluster.data[0] == 0x00){
|
||||
log.debug "ON/OFF REPORTING CONFIG RESPONSE: $cluster"
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
else {
|
||||
log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.info "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug zigbeeMap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def on() {
|
||||
zigbee.on()
|
||||
}
|
||||
|
||||
def off() {
|
||||
zigbee.off()
|
||||
}
|
||||
/**
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
return zigbee.onOffRefresh()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01)
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||
sendEvent(name: "checkInterval", value: 3 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
|
||||
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
zigbee.setLevel(value)
|
||||
}
|
||||
|
||||
def setColor(value){
|
||||
log.trace "setColor($value)"
|
||||
zigbee.on() + setHue(value.hue) + "delay 500" + setSaturation(value.saturation)
|
||||
}
|
||||
|
||||
def setHue(value) {
|
||||
def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5)
|
||||
}
|
||||
|
||||
def setSaturation(value) {
|
||||
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + "delay 1000" + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
}
|
||||
2
devicetypes/smartthings/zigbee-rgbw-bulb.src/.st-ignore
Normal file
2
devicetypes/smartthings/zigbee-rgbw-bulb.src/.st-ignore
Normal file
@@ -0,0 +1,2 @@
|
||||
.st-ignore
|
||||
README.md
|
||||
42
devicetypes/smartthings/zigbee-rgbw-bulb.src/README.md
Normal file
42
devicetypes/smartthings/zigbee-rgbw-bulb.src/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# OSRAM LIGHTIFY LED RGBW Bulb
|
||||
|
||||
|
||||
|
||||
Works with:
|
||||
|
||||
* [OSRAM LIGHTIFY LED RGBW Bulb](https://support.smartthings.com/hc/en-us/articles/207728173-OSRAM-LIGHTIFY-LED-Smart-Connected-Light-A19-RGBW)
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Capabilities](#capabilities)
|
||||
* [Health](#device-health)
|
||||
* [Battery](#battery-specification)
|
||||
|
||||
## Capabilities
|
||||
|
||||
* **Actuator** - It represents that a device has commands.
|
||||
* **Color Control** - It represents that the color attributes of a device can be controlled (hue, saturation, color value).
|
||||
* **Color Temperature** - It represents color temperature capability measured in degree Kelvin.
|
||||
* **Polling** - It represents that a device can be polled.
|
||||
* **Switch** - can detect state (possible values: on/off)
|
||||
* **Switch Level** - can detect current light level (0-100 in percent)
|
||||
* **Configuration** - _configure()_ command called when device is installed or device preferences updated
|
||||
* **Refresh** - _refresh()_ command for status updates
|
||||
* **Health Check** - indicates ability to get device health notifications
|
||||
|
||||
|
||||
## Device Health
|
||||
|
||||
A Category C6 OSRAM LIGHTIFY LED RGBW Bulb with maxReportTime of 5 mins.
|
||||
Check-in interval is double the value of maxReportTime.
|
||||
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||
Check-in interval = 12 mins
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range.
|
||||
Pairing needs to be tried again by placing the device closer to the hub.
|
||||
It may also happen that you need to reset the device.
|
||||
Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link:
|
||||
* [Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/207728173-OSRAM-LIGHTIFY-LED-Smart-Connected-Light-A19-RGBW)
|
||||
@@ -85,12 +85,6 @@ def parse(String description) {
|
||||
def event = zigbee.getEvent(description)
|
||||
if (event) {
|
||||
log.debug event
|
||||
// Temporary fix for the case when Device is OFFLINE and is connected again
|
||||
if (state.lastActivity == null){
|
||||
state.lastActivity = now()
|
||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
||||
}
|
||||
state.lastActivity = now()
|
||||
if (event.name=="level" && event.value==0) {}
|
||||
else {
|
||||
if (event.name=="colorTemperature") {
|
||||
@@ -101,11 +95,11 @@ def parse(String description) {
|
||||
}
|
||||
else {
|
||||
def zigbeeMap = zigbee.parseDescriptionAsMap(description)
|
||||
log.trace "zigbeeMap : $zigbeeMap"
|
||||
def cluster = zigbee.parse(description)
|
||||
|
||||
if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) {
|
||||
if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute
|
||||
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 360)
|
||||
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||
sendEvent(name: "hue", value: hueValue, descriptionText: "Color has changed")
|
||||
}
|
||||
else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute
|
||||
@@ -113,8 +107,18 @@ def parse(String description) {
|
||||
sendEvent(name: "saturation", value: saturationValue, descriptionText: "Color has changed", displayed: false)
|
||||
}
|
||||
}
|
||||
else if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) {
|
||||
if (cluster.data[0] == 0x00){
|
||||
log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
else {
|
||||
log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.info "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug zigbeeMap
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,26 +134,21 @@ def off() {
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
|
||||
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
|
||||
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
|
||||
state.lastActivity = null
|
||||
return zigbee.onOffRefresh()
|
||||
} else {
|
||||
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
|
||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
||||
}
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
zigbee.onOffRefresh() + zigbee.readAttribute(0x0008, 0x00) + zigbee.readAttribute(0x0300, 0x00) + zigbee.readAttribute(0x0300, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(0x0300, ATTRIBUTE_HUE) + zigbee.readAttribute(0x0300, ATTRIBUTE_SATURATION) + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01)
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01)
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
// Enrolls device to Device-Watch with 3 x Reporting interval 30min
|
||||
sendEvent(name: "checkInterval", value: 1800, displayed: false, data: [protocol: "zigbee"])
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01) + zigbee.readAttribute(0x0006, 0x00) + zigbee.readAttribute(0x0008, 0x00) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, 0x00) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||
sendEvent(name: "checkInterval", value: 3 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
|
||||
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
refresh()
|
||||
}
|
||||
|
||||
def setColorTemperature(value) {
|
||||
@@ -190,5 +189,5 @@ def setHue(value) {
|
||||
|
||||
def setSaturation(value) {
|
||||
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") //payload-> sat value, transition time
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + "delay 1000" + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ metadata {
|
||||
capability "Power Meter"
|
||||
capability "Sensor"
|
||||
capability "Switch"
|
||||
capability "Health Check"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B04"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702"
|
||||
@@ -77,10 +78,28 @@ def on() {
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
zigbee.onOffRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig()
|
||||
Integer reportIntervalMinutes = 5
|
||||
zigbee.onOffRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig(0,reportIntervalMinutes * 60) + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
zigbee.onOffConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() + zigbee.onOffRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh()
|
||||
log.debug "in configure()"
|
||||
return configureHealthCheck()
|
||||
}
|
||||
|
||||
def configureHealthCheck() {
|
||||
Integer hcIntervalMinutes = 12
|
||||
sendEvent(name: "checkInterval", value: hcIntervalMinutes * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
return refresh()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "in updated()"
|
||||
// updated() doesn't have it's return value processed as hub commands, so we have to send them explicitly
|
||||
def cmds = configureHealthCheck()
|
||||
cmds.each{ sendHubCommand(new physicalgraph.device.HubAction(it)) }
|
||||
}
|
||||
|
||||
def ping() {
|
||||
return zigbee.onOffRefresh()
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ metadata {
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0006", outClusters: "0003, 0006, 0019, 0406", manufacturer: "Leviton", model: "ZSS-10", deviceJoinName: "Leviton Switch"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0006", outClusters: "000A", manufacturer: "HAI", model: "65A21-1", deviceJoinName: "Leviton Wireless Load Control Module-30amp"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
@@ -78,5 +79,5 @@ def refresh() {
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
zigbee.onOffConfig() + zigbee.onOffRefresh()
|
||||
zigbee.onOffRefresh() + zigbee.onOffConfig()
|
||||
}
|
||||
|
||||
@@ -134,10 +134,5 @@ def refresh() {
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
zigbee.onOffConfig() +
|
||||
zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING, TYPE_U8, 600, 21600, 1) +
|
||||
zigbee.configureReporting(CLUSTER_BASIC, BASIC_ATTR_POWER_SOURCE, TYPE_ENUM8, 5, 21600, 1) +
|
||||
zigbee.onOffRefresh() +
|
||||
zigbee.readAttribute(CLUSTER_BASIC, BASIC_ATTR_POWER_SOURCE) +
|
||||
zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING)
|
||||
refresh()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.st-ignore
|
||||
README.md
|
||||
@@ -0,0 +1,37 @@
|
||||
# OSRAM Lightify Tunable 60 White
|
||||
|
||||
|
||||
|
||||
Works with:
|
||||
|
||||
* [OSRAM Lightify Tunable 60 White](http://www.osram.com/osram_com/tools-and-services/tools/lightify---smart-connected-light/lightify-for-home---what-is-light-to-you/lightify-products/lightify-classic-a60-tunable-white/index.jsp)
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Capabilities](#capabilities)
|
||||
* [Health](#device-health)
|
||||
* [Battery](#battery-specification)
|
||||
|
||||
## Capabilities
|
||||
|
||||
* **Actuator** - represents that a Device has commands
|
||||
* **Color Temperature** - represents color temperature, measured in degrees Kelvin.
|
||||
* **Configuration** - _configure()_ command called when device is installed or device preferences updated.
|
||||
* **Refresh** - _refresh()_ command for status updates
|
||||
* **Switch** - can detect state (possible values: on/off)
|
||||
* **Switch Level** - represents current light level, usually 0-100 in percent
|
||||
* **Health Check** - indicates ability to get device health notifications
|
||||
|
||||
## Device Health
|
||||
|
||||
A Category C1 OSRAM Lightify Tunable 60 White with maxReportTime of 5 mins.
|
||||
Check-in interval is double the value of maxReportTime.
|
||||
This gives the device twice the amount of time to respond before it is marked as offline.
|
||||
Check-in interval = 12 mins
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range.
|
||||
Pairing needs to be tried again by placing the device closer to the hub.
|
||||
Other troubleshooting tips are listed as follows:
|
||||
* [Troubleshooting:](https://support.smartthings.com/hc/en-us/articles/204576454-OSRAM-LIGHTIFY-Tunable-White-60-Bulb)
|
||||
@@ -74,12 +74,6 @@ def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
def event = zigbee.getEvent(description)
|
||||
if (event) {
|
||||
// Temporary fix for the case when Device is OFFLINE and is connected again
|
||||
if (state.lastActivity == null){
|
||||
state.lastActivity = now()
|
||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
||||
}
|
||||
state.lastActivity = now()
|
||||
if (event.name=="level" && event.value==0) {}
|
||||
else {
|
||||
if (event.name=="colorTemperature") {
|
||||
@@ -88,9 +82,22 @@ def parse(String description) {
|
||||
sendEvent(event)
|
||||
}
|
||||
}
|
||||
else {
|
||||
def cluster = zigbee.parse(description)
|
||||
|
||||
if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) {
|
||||
if (cluster.data[0] == 0x00) {
|
||||
log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster
|
||||
sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
else {
|
||||
log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}"
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug zigbee.parseDescriptionAsMap(description)
|
||||
log.debug "${cluster}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,26 +117,21 @@ def setLevel(value) {
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
|
||||
if (state.lastActivity < (now() - (1000 * device.currentValue("checkInterval"))) ){
|
||||
log.info "ping, alive=no, lastActivity=${state.lastActivity}"
|
||||
state.lastActivity = null
|
||||
return zigbee.onOffRefresh()
|
||||
} else {
|
||||
log.info "ping, alive=yes, lastActivity=${state.lastActivity}"
|
||||
sendEvent(name: "deviceWatch-lastActivity", value: state.lastActivity, description: "Last Activity is on ${new Date((long)state.lastActivity)}", displayed: false, isStateChange: true)
|
||||
}
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig()
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.colorTemperatureConfig()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
// Enrolls device to Device-Watch with 3 x Reporting interval 30min
|
||||
sendEvent(name: "checkInterval", value: 1800, displayed: false, data: [protocol: "zigbee"])
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
|
||||
// Device-Watch allows 3 check-in misses from device (plus 1 min lag time)
|
||||
// enrolls with default periodic reporting until newer 5 min interval is confirmed
|
||||
sendEvent(name: "checkInterval", value: 3 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
|
||||
// OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity
|
||||
refresh()
|
||||
}
|
||||
|
||||
def setColorTemperature(value) {
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
import groovy.transform.Field
|
||||
|
||||
@Field Boolean hasConfiguredHealthCheck = false
|
||||
|
||||
metadata {
|
||||
definition (name: "ZLL Dimmer Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||
@@ -21,6 +24,7 @@ metadata {
|
||||
capability "Refresh"
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
capability "Health Check"
|
||||
|
||||
//fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0000,0019"
|
||||
fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019"
|
||||
@@ -96,7 +100,38 @@ def poll() {
|
||||
refresh()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
/**
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
return zigbee.levelRefresh()
|
||||
}
|
||||
|
||||
def healthPoll() {
|
||||
log.debug "healthPoll()"
|
||||
def cmds = refresh()
|
||||
cmds.each{ sendHubCommand(new physicalgraph.device.HubAction(it))}
|
||||
}
|
||||
|
||||
def configureHealthCheck() {
|
||||
Integer hcIntervalMinutes = 12
|
||||
if (!hasConfiguredHealthCheck) {
|
||||
log.debug "Configuring Health Check, Reporting"
|
||||
unschedule("healthPoll")
|
||||
runEvery5Minutes("healthPoll")
|
||||
// Device-Watch allows 2 check-in misses from device
|
||||
sendEvent(name: "checkInterval", value: hcIntervalMinutes * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
hasConfiguredHealthCheck = true
|
||||
}
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "configure()"
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
configureHealthCheck()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "updated()"
|
||||
configureHealthCheck()
|
||||
}
|
||||
|
||||
134
devicetypes/smartthings/zll-rgb-bulb.src/zll-rgb-bulb.groovy
Normal file
134
devicetypes/smartthings/zll-rgb-bulb.src/zll-rgb-bulb.groovy
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Copyright 2016 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "ZLL RGB Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
capability "Actuator"
|
||||
capability "Color Control"
|
||||
capability "Configuration"
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel"
|
||||
}
|
||||
tileAttribute ("device.color", key: "COLOR_CONTROL") {
|
||||
attributeState "color", action:"color control.setColor"
|
||||
}
|
||||
}
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
//Globals
|
||||
private getATTRIBUTE_HUE() { 0x0000 }
|
||||
private getATTRIBUTE_SATURATION() { 0x0001 }
|
||||
private getHUE_COMMAND() { 0x00 }
|
||||
private getSATURATION_COMMAND() { 0x03 }
|
||||
private getCOLOR_CONTROL_CLUSTER() { 0x0300 }
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def finalResult = zigbee.getEvent(description)
|
||||
if (finalResult) {
|
||||
log.debug finalResult
|
||||
sendEvent(finalResult)
|
||||
}
|
||||
else {
|
||||
def zigbeeMap = zigbee.parseDescriptionAsMap(description)
|
||||
log.trace "zigbeeMap : $zigbeeMap"
|
||||
|
||||
if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) {
|
||||
if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute
|
||||
def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 360)
|
||||
sendEvent(name: "hue", value: hueValue, displayed:false)
|
||||
}
|
||||
else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute
|
||||
def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 255 * 100)
|
||||
sendEvent(name: "saturation", value: saturationValue, displayed:false)
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.info "DID NOT PARSE MESSAGE for description : $description"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def on() {
|
||||
zigbee.on() + ["delay 1500"] + zigbee.onOffRefresh()
|
||||
}
|
||||
|
||||
def off() {
|
||||
zigbee.off() + ["delay 1500"] + zigbee.onOffRefresh()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
refreshAttributes() + configureAttributes()
|
||||
}
|
||||
|
||||
def poll() {
|
||||
refreshAttributes()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
configureAttributes() + refreshAttributes()
|
||||
}
|
||||
|
||||
def configureAttributes() {
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, 0x20, 1, 3600, 0x01) + zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, 0x20, 1, 3600, 0x01)
|
||||
}
|
||||
|
||||
def refreshAttributes() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
zigbee.setLevel(value) + ["delay 1500"] + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report
|
||||
}
|
||||
|
||||
def setColor(value){
|
||||
log.trace "setColor($value)"
|
||||
zigbee.on() + setHue(value.hue) + ["delay 300"] + setSaturation(value.saturation) + ["delay 2000"] + refreshAttributes()
|
||||
}
|
||||
|
||||
def setHue(value) {
|
||||
def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5)
|
||||
}
|
||||
|
||||
def setSaturation(value) {
|
||||
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) //payload-> sat value, transition time
|
||||
}
|
||||
@@ -123,7 +123,7 @@ def configureAttributes() {
|
||||
}
|
||||
|
||||
def refreshAttributes() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.readAttribute(0x0300, 0x00) + zigbee.readAttribute(0x0300, ATTRIBUTE_HUE) + zigbee.readAttribute(0x0300, ATTRIBUTE_SATURATION)
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION)
|
||||
}
|
||||
|
||||
def setColorTemperature(value) {
|
||||
@@ -141,10 +141,10 @@ def setColor(value){
|
||||
|
||||
def setHue(value) {
|
||||
def scaledHueValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, scaledHueValue, "00", "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) (0500 in U16 reads 5)
|
||||
}
|
||||
|
||||
def setSaturation(value) {
|
||||
def scaledSatValue = zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2)
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") //payload-> sat value, transition time
|
||||
zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, scaledSatValue, "0500") + ["delay 1500"] + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) //payload-> sat value, transition time
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
import groovy.transform.Field
|
||||
|
||||
@Field Boolean hasConfiguredHealthCheck = false
|
||||
|
||||
metadata {
|
||||
definition (name: "ZLL White Color Temperature Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||
@@ -22,6 +25,7 @@ metadata {
|
||||
capability "Refresh"
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
capability "Health Check"
|
||||
|
||||
attribute "colorName", "string"
|
||||
command "setGenericName"
|
||||
@@ -96,9 +100,41 @@ def poll() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
|
||||
}
|
||||
|
||||
/**
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
return zigbee.levelRefresh()
|
||||
}
|
||||
|
||||
def healthPoll() {
|
||||
log.debug "healthPoll()"
|
||||
def cmds = zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
cmds.each{ sendHubCommand(new physicalgraph.device.HubAction(it))}
|
||||
}
|
||||
|
||||
def configureHealthCheck() {
|
||||
Integer hcIntervalMinutes = 12
|
||||
if (!hasConfiguredHealthCheck) {
|
||||
log.debug "Configuring Health Check, Reporting"
|
||||
unschedule("healthPoll")
|
||||
runEvery5Minutes("healthPoll")
|
||||
// Device-Watch allows 2 check-in misses from device
|
||||
sendEvent(name: "checkInterval", value: hcIntervalMinutes * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
|
||||
hasConfiguredHealthCheck = true
|
||||
}
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
log.debug "configure()"
|
||||
configureHealthCheck()
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
|
||||
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "updated()"
|
||||
configureHealthCheck()
|
||||
}
|
||||
|
||||
def setColorTemperature(value) {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.st-ignore
|
||||
README.md
|
||||
@@ -0,0 +1,39 @@
|
||||
# Z-wave Dimmer
|
||||
|
||||
|
||||
|
||||
Works with:
|
||||
|
||||
* [Leviton Plug-in Lamp Dimmer Module (DZPD3-1LW)](http://www.leviton.com/OA_HTML/ProductDetail.jsp?partnumber=DZPD3-1LW)
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Capabilities](#capabilities)
|
||||
* [Health](#device-health)
|
||||
* [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Capabilities
|
||||
|
||||
* **Switch Level** - it's defined to accept two parameters, the level and the rate of dimming
|
||||
* **Actuator** - represents that a Device has commands
|
||||
* **Health Check** - indicates ability to get device health notifications
|
||||
* **Switch** - can detect state (possible values: on/off)
|
||||
* **Polling** - represents that poll() can be implemented for the device
|
||||
* **Refresh** - _refresh()_ command for status updates
|
||||
* **Sensor** - detects sensor events
|
||||
|
||||
## Device Health
|
||||
|
||||
A Category C5 Leviton Plug-in Lamp Dimmer Module (DZPA1-1LW) (Z-Wave) polled by the hub.
|
||||
As of hubCore version 0.14.38 the hub sends up reports every 15 minutes regardless of whether the state changed.
|
||||
Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*15 + 2)mins = 32 mins.
|
||||
Not to mention after going OFFLINE when the device is plugged back in, it might take a considerable amount of time for
|
||||
the device to appear as ONLINE again. This is because if this listening device does not respond to two poll requests in a row,
|
||||
it is not polled for 5 minutes by the hub. This can delay up the process of being marked ONLINE by quite some time.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range.
|
||||
Pairing needs to be tried again by placing the device closer to the hub.
|
||||
Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link:
|
||||
* [Leviton Plug-in Lamp Dimmer Module (DZPD3-1LW) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/206171053-How-to-connect-Leviton-Z-Wave-devices)
|
||||
@@ -15,12 +15,14 @@ metadata {
|
||||
definition (name: "Z-Wave Dimmer Switch Generic", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Switch Level"
|
||||
capability "Actuator"
|
||||
capability "Health Check"
|
||||
capability "Switch"
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
|
||||
fingerprint inClusters: "0x26", deviceJoinName: "Z-Wave Dimmer"
|
||||
fingerprint mfr:"001D", prod:"1902", deviceJoinName: "Z-Wave Dimmer"
|
||||
}
|
||||
|
||||
simulator {
|
||||
@@ -68,6 +70,11 @@ metadata {
|
||||
}
|
||||
}
|
||||
|
||||
def updated(){
|
||||
// Device-Watch simply pings if no device events received for 32min(checkInterval)
|
||||
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
def result = null
|
||||
if (description != "updated") {
|
||||
@@ -185,6 +192,13 @@ def poll() {
|
||||
zwave.switchMultilevelV1.switchMultilevelGet().format()
|
||||
}
|
||||
|
||||
/**
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
* */
|
||||
def ping() {
|
||||
refresh()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "refresh() is called"
|
||||
def commands = []
|
||||
|
||||
2
devicetypes/smartthings/zwave-switch.src/.st-ignore
Normal file
2
devicetypes/smartthings/zwave-switch.src/.st-ignore
Normal file
@@ -0,0 +1,2 @@
|
||||
.st-ignore
|
||||
README.md
|
||||
49
devicetypes/smartthings/zwave-switch.src/README.md
Normal file
49
devicetypes/smartthings/zwave-switch.src/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Z-Wave Switch
|
||||
|
||||
|
||||
Works with:
|
||||
|
||||
* [GE Z-Wave Plug-In Smart Switch (12719)](http://products.z-wavealliance.org/products/1193)
|
||||
* [GE Z-Wave In-Wall Smart Outlet (12721)](http://products.z-wavealliance.org/products/1195)
|
||||
* [GE Z-Wave In-Wall Smart Switch (12722)](http://products.z-wavealliance.org/products/1196)
|
||||
* [GE Z-Wave In-Wall Smart Toggle Switch (12727)](http://products.z-wavealliance.org/products/1200)
|
||||
|
||||
|
||||
## Table of contents
|
||||
|
||||
* [Capabilities](#capabilities)
|
||||
* [Health](#device-health)
|
||||
* [Troubleshooting](#Troubleshooting)
|
||||
|
||||
## Capabilities
|
||||
|
||||
* **Actuator** - represents that a Device has commands
|
||||
* **Indicator** - gives you the ability to set the indicator LED light on a Z-Wave switch
|
||||
* **Switch** - can detect state (possible values: on/off)
|
||||
* **Polling** - represents that poll() can be implemented for the device
|
||||
* **Refresh** - _refresh()_ command for status updates
|
||||
* **Sensor** - detects sensor events
|
||||
* **Health Check** - indicates ability to get device health notifications
|
||||
|
||||
## Device Health
|
||||
|
||||
Z-Wave Switches (Plug-In, In-Wall(Toggle Switch, Switch, Outlet)) are polled by the hub.
|
||||
As of hubCore version 0.14.38 the hub sends up reports every 15 minutes regardless of whether the state changed.
|
||||
Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*15 + 2)mins = 32 mins.
|
||||
Not to mention after going OFFLINE when the device is plugged back in, it might take a considerable amount of time for
|
||||
the device to appear as ONLINE again. This is because if this listening device does not respond to two poll requests in a row,
|
||||
it is not polled for 5 minutes by the hub. This can delay up the process of being marked ONLINE by quite some time.
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range.
|
||||
Pairing needs to be tried again by placing the device closer to the hub.
|
||||
Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link:
|
||||
* [General Z-Wave Dimmer/Switch Troubleshooting](https://support.smartthings.com/hc/en-us/articles/200955890-Troubleshooting-GE-in-wall-switch-or-dimmer-won-t-respond-to-commands-or-automations-Z-Wave-)
|
||||
* [GE Z-Wave Plug-In Smart Switch (12719) Troubleshooting](https://support.smartthings.com/hc/en-us/articles/200903070-GE-Plug-In-Smart-Switch-GE-12719-Z-Wave)
|
||||
* [GE Z-Wave In-Wall Smart Outlet (12721) Troubleshooting](https://support.smartthings.com/hc/en-us/articles/200903020-GE-In-Wall-Smart-Outlet-GE-12721-Z-Wave)
|
||||
* [GE Z-Wave In-Wall Smart Switch (12722) Troubleshooting](https://support.smartthings.com/hc/en-us/articles/200902540-GE-In-Wall-Smart-Switch-GE-12722-Z-Wave)
|
||||
* [GE Z-Wave In-Wall Smart Toggle Switch (12727) Troubleshooting](https://support.smartthings.com/hc/en-us/articles/207568933-GE-In-Wall-Smart-Toggle-Switch-GE-12727-Z-Wave)
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ metadata {
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Health Check"
|
||||
|
||||
fingerprint mfr:"0063", prod:"4952", deviceJoinName: "Z-Wave Wall Switch"
|
||||
fingerprint mfr:"0063", prod:"5257", deviceJoinName: "Z-Wave Wall Switch"
|
||||
@@ -64,6 +65,8 @@ metadata {
|
||||
}
|
||||
|
||||
def updated(){
|
||||
// Device-Watch simply pings if no device events received for 32min(checkInterval)
|
||||
sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID])
|
||||
switch (ledIndicator) {
|
||||
case "on":
|
||||
indicatorWhenOn()
|
||||
@@ -156,6 +159,13 @@ def poll() {
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* PING is used by Device-Watch in attempt to reach the Device
|
||||
**/
|
||||
def ping() {
|
||||
refresh()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
delayBetween([
|
||||
zwave.switchBinaryV1.switchBinaryGet().format(),
|
||||
|
||||
@@ -65,7 +65,16 @@ void updateSwitch() {
|
||||
private void updateAll(devices) {
|
||||
def command = request.JSON?.command
|
||||
if (command) {
|
||||
devices."$command"()
|
||||
switch(command) {
|
||||
case "on":
|
||||
devices.on()
|
||||
break
|
||||
case "off":
|
||||
devices.off()
|
||||
break
|
||||
default:
|
||||
httpError(403, "Access denied. This command is not supported by current capability.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +86,16 @@ private void update(devices) {
|
||||
if (!device) {
|
||||
httpError(404, "Device not found")
|
||||
} else {
|
||||
device."$command"()
|
||||
switch(command) {
|
||||
case "on":
|
||||
device.on()
|
||||
break
|
||||
case "off":
|
||||
device.off()
|
||||
break
|
||||
default:
|
||||
httpError(403, "Access denied. This command is not supported by current capability.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ def authPage() {
|
||||
if (canInstallLabs()) {
|
||||
|
||||
def redirectUrl = getBuildRedirectUrl()
|
||||
log.debug "Redirect url = ${redirectUrl}"
|
||||
// log.debug "Redirect url = ${redirectUrl}"
|
||||
|
||||
if (state.authToken) {
|
||||
description = "Tap 'Next' to proceed"
|
||||
@@ -113,13 +113,13 @@ def oauthInitUrl() {
|
||||
scope: "read_station"
|
||||
]
|
||||
|
||||
log.debug "REDIRECT URL: ${getVendorAuthPath() + toQueryString(oauthParams)}"
|
||||
// log.debug "REDIRECT URL: ${getVendorAuthPath() + toQueryString(oauthParams)}"
|
||||
|
||||
redirect (location: getVendorAuthPath() + toQueryString(oauthParams))
|
||||
}
|
||||
|
||||
def callback() {
|
||||
log.debug "callback()>> params: $params, params.code ${params.code}"
|
||||
// log.debug "callback()>> params: $params, params.code ${params.code}"
|
||||
|
||||
def code = params.code
|
||||
def oauthState = params.state
|
||||
@@ -135,7 +135,7 @@ def callback() {
|
||||
scope: "read_station"
|
||||
]
|
||||
|
||||
log.debug "TOKEN URL: ${getVendorTokenPath() + toQueryString(tokenParams)}"
|
||||
// log.debug "TOKEN URL: ${getVendorTokenPath() + toQueryString(tokenParams)}"
|
||||
|
||||
def tokenUrl = getVendorTokenPath()
|
||||
def params = [
|
||||
@@ -144,7 +144,7 @@ def callback() {
|
||||
body: tokenParams
|
||||
]
|
||||
|
||||
log.debug "PARAMS: ${params}"
|
||||
// log.debug "PARAMS: ${params}"
|
||||
|
||||
httpPost(params) { resp ->
|
||||
|
||||
@@ -156,7 +156,7 @@ def callback() {
|
||||
state.refreshToken = data.refresh_token
|
||||
state.authToken = data.access_token
|
||||
state.tokenExpires = now() + (data.expires_in * 1000)
|
||||
log.debug "swapped token: $resp.data"
|
||||
// log.debug "swapped token: $resp.data"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,7 +292,7 @@ def refreshToken() {
|
||||
|
||||
response.data.each {key, value ->
|
||||
def data = slurper.parseText(key);
|
||||
log.debug "Data: $data"
|
||||
// log.debug "Data: $data"
|
||||
|
||||
state.refreshToken = data.refresh_token
|
||||
state.accessToken = data.access_token
|
||||
|
||||
@@ -21,13 +21,18 @@ definition(
|
||||
name: "Smart Windows",
|
||||
namespace: "egid",
|
||||
author: "Eric Gideon",
|
||||
description: "Compares two temperatures – indoor vs outdoor, for example – then sends an alert if windows are open (or closed!). If you don't use an external temperature device, your zipcode will be used instead.",
|
||||
description: "Compares two temperatures – indoor vs outdoor, for example – then sends an alert if windows are open (or closed!). If you don't use an external temperature device, your location will be used instead.",
|
||||
iconUrl: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn@2x.png"
|
||||
)
|
||||
|
||||
|
||||
preferences {
|
||||
|
||||
if (!(location.zipCode || ( location.latitude && location.longitude )) && location.channelName == 'samsungtv') {
|
||||
section { paragraph title: "Note:", "Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location." }
|
||||
}
|
||||
|
||||
section( "Set the temperature range for your comfort zone..." ) {
|
||||
input "minTemp", "number", title: "Minimum temperature"
|
||||
input "maxTemp", "number", title: "Maximum temperature"
|
||||
@@ -39,9 +44,11 @@ preferences {
|
||||
input "inTemp", "capability.temperatureMeasurement", title: "Indoor"
|
||||
input "outTemp", "capability.temperatureMeasurement", title: "Outdoor (optional)", required: false
|
||||
}
|
||||
section( "Set your location" ) {
|
||||
input "zipCode", "text", title: "Zip code"
|
||||
|
||||
if (location.channelName != 'samsungtv') {
|
||||
section( "Set your location" ) { input "zipCode", "text", title: "Zip code" }
|
||||
}
|
||||
|
||||
section( "Notifications" ) {
|
||||
input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false
|
||||
input "retryPeriod", "number", title: "Minutes between notifications:"
|
||||
@@ -125,7 +132,11 @@ def temperatureHandler(evt) {
|
||||
}
|
||||
|
||||
def weatherCheck() {
|
||||
def json = getWeatherFeature("conditions", zipCode)
|
||||
def json
|
||||
if (location.channelName != 'samsungtv')
|
||||
json = getWeatherFeature("conditions", zipCode)
|
||||
else
|
||||
json = getWeatherFeature("conditions")
|
||||
def currentTemp = json?.current_observation?.temp_f
|
||||
|
||||
if ( currentTemp ) {
|
||||
|
||||
253
smartapps/gideon-api/gideon.src/gideon.groovy
Normal file
253
smartapps/gideon-api/gideon.src/gideon.groovy
Normal file
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* Gideon
|
||||
*
|
||||
* Copyright 2016 Nicola Russo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Gideon",
|
||||
namespace: "gideon.api",
|
||||
author: "Braindrain Solutions",
|
||||
description: "Gideon AI Smart app allows you to connect and control all of your SmartThings devices through the Gideon AI app, making your SmartThings devices even smarter.",
|
||||
category: "Family",
|
||||
iconUrl: "http://s33.postimg.org/t77u7y7v3/logo.png",
|
||||
iconX2Url: "http://s33.postimg.org/t77u7y7v3/logo.png",
|
||||
iconX3Url: "http://s33.postimg.org/t77u7y7v3/logo.png",
|
||||
oauth: [displayName: "Gideon AI API", displayLink: "gideon.ai"])
|
||||
|
||||
|
||||
preferences {
|
||||
section("Control these switches...") {
|
||||
input "switches", "capability.switch", multiple:true
|
||||
}
|
||||
section("Control these motion sensors...") {
|
||||
input "motions", "capability.motionSensor", multiple:true
|
||||
}
|
||||
section("Control these presence sensors...") {
|
||||
input "presence_sensors", "capability.presenceSensor", multiple:true
|
||||
}
|
||||
section("Control these outlets...") {
|
||||
input "outlets", "capability.switch", multiple:true
|
||||
}
|
||||
section("Control these locks...") {
|
||||
input "locks", "capability.lock", multiple:true
|
||||
}
|
||||
section("Control these locks...") {
|
||||
input "temperature_sensors", "capability.temperatureMeasurement"
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
// TODO: subscribe to attributes, devices, locations, etc.
|
||||
subscribe(outlet, "energy", outletHandler)
|
||||
subscribe(outlet, "switch", outletHandler)
|
||||
}
|
||||
|
||||
// TODO: implement event handlers
|
||||
def outletHandler(evt) {
|
||||
log.debug "$outlet.currentEnergy"
|
||||
//TODO call G API
|
||||
}
|
||||
|
||||
|
||||
private device(it, type) {
|
||||
it ? [id: it.id, label: it.label, type: type] : null
|
||||
}
|
||||
|
||||
//API Mapping
|
||||
mappings {
|
||||
path("/getalldevices") {
|
||||
action: [
|
||||
GET: "getAllDevices"
|
||||
]
|
||||
}
|
||||
path("/doorlocks/:id/:command") {
|
||||
action: [
|
||||
GET: "updateDoorLock"
|
||||
]
|
||||
}
|
||||
path("/doorlocks/:id") {
|
||||
action: [
|
||||
GET: "getDoorLockStatus"
|
||||
]
|
||||
}
|
||||
path("/tempsensors/:id") {
|
||||
action: [
|
||||
GET: "getTempSensorsStatus"
|
||||
]
|
||||
}
|
||||
path("/presences/:id") {
|
||||
action: [
|
||||
GET: "getPresenceStatus"
|
||||
]
|
||||
}
|
||||
path("/motions/:id") {
|
||||
action: [
|
||||
GET: "getMotionStatus"
|
||||
]
|
||||
}
|
||||
path("/outlets/:id") {
|
||||
action: [
|
||||
GET: "getOutletStatus"
|
||||
]
|
||||
}
|
||||
path("/outlets/:id/:command") {
|
||||
action: [
|
||||
GET: "updateOutlet"
|
||||
]
|
||||
}
|
||||
path("/switches/:command") {
|
||||
action: [
|
||||
PUT: "updateSwitch"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
//API Methods
|
||||
def getAllDevices() {
|
||||
def locks_list = locks.collect{device(it,"Lock")}
|
||||
def presences_list = presence_sensors.collect{device(it,"Presence")}
|
||||
def motions_list = motions.collect{device(it,"Motion")}
|
||||
def outlets_list = outlets.collect{device(it,"Outlet")}
|
||||
def switches_list = switches.collect{device(it,"Switch")}
|
||||
def temp_list = temperature_sensors.collect{device(it,"Temperature")}
|
||||
return [Locks: locks_list, Presences: presences_list, Motions: motions_list, Outlets: outlets_list, Switches: switches_list, Temperatures: temp_list]
|
||||
}
|
||||
|
||||
//LOCKS
|
||||
def getDoorLockStatus() {
|
||||
def device = locks.find { it.id == params.id }
|
||||
if (!device) {
|
||||
httpError(404, "Device not found")
|
||||
} else {
|
||||
return [Device_state: device.currentValue('lock')]
|
||||
}
|
||||
}
|
||||
|
||||
def updateDoorLock() {
|
||||
def command = params.command
|
||||
def device = locks.find { it.id == params.id }
|
||||
if (command){
|
||||
if (!device) {
|
||||
httpError(404, "Device not found")
|
||||
} else {
|
||||
if(command == "toggle")
|
||||
{
|
||||
if(device.currentValue('lock') == "locked")
|
||||
device.unlock();
|
||||
else
|
||||
device.lock();
|
||||
|
||||
return [Device_id: params.id, result_action: "200"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//PRESENCE
|
||||
def getPresenceStatus() {
|
||||
|
||||
def device = presence_sensors.find { it.id == params.id }
|
||||
if (!device) {
|
||||
httpError(404, "Device not found")
|
||||
} else {
|
||||
return [Device_state: device.currentValue('presence')]
|
||||
}
|
||||
}
|
||||
|
||||
//MOTION
|
||||
def getMotionStatus() {
|
||||
|
||||
def device = motions.find { it.id == params.id }
|
||||
if (!device) {
|
||||
httpError(404, "Device not found")
|
||||
} else {
|
||||
return [Device_state: device.currentValue('motion')]
|
||||
}
|
||||
}
|
||||
|
||||
//OUTLET
|
||||
def getOutletStatus() {
|
||||
|
||||
def device = outlets.find { it.id == params.id }
|
||||
if (!device) {
|
||||
httpError(404, "Device not found")
|
||||
} else {
|
||||
return [Device_state: device.currentSwitch, Current_watt: device.currentValue("energy")]
|
||||
}
|
||||
}
|
||||
|
||||
def updateOutlet() {
|
||||
|
||||
def command = params.command
|
||||
def device = outlets.find { it.id == params.id }
|
||||
if (command){
|
||||
if (!device) {
|
||||
httpError(404, "Device not found")
|
||||
} else {
|
||||
if(command == "toggle")
|
||||
{
|
||||
if(device.currentSwitch == "on")
|
||||
device.off();
|
||||
else
|
||||
device.on();
|
||||
|
||||
return [Device_id: params.id, result_action: "200"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//SWITCH
|
||||
def updateSwitch() {
|
||||
def command = params.command
|
||||
def device = switches.find { it.id == params.id }
|
||||
if (command){
|
||||
if (!device) {
|
||||
httpError(404, "Device not found")
|
||||
} else {
|
||||
if(command == "toggle")
|
||||
{
|
||||
if(device.currentSwitch == "on")
|
||||
device.off();
|
||||
else
|
||||
device.on();
|
||||
|
||||
return [Device_id: params.id, result_action: "200"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TEMPERATURE
|
||||
def getTempSensorsStatus() {
|
||||
|
||||
def device = temperature_sensors.find { it.id == params.id }
|
||||
if (!device) {
|
||||
httpError(404, "Device not found")
|
||||
} else {
|
||||
return [Device_state: device.currentValue('temperature')]
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,13 @@ definition(
|
||||
)
|
||||
|
||||
preferences {
|
||||
section("Zip code?") {
|
||||
input "zipcode", "text", title: "Zipcode?"
|
||||
|
||||
if (!(location.zipCode || ( location.latitude && location.longitude )) && location.channelName == 'samsungtv') {
|
||||
section { paragraph title: "Note:", "Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location." }
|
||||
}
|
||||
|
||||
if (location.channelName != 'samsungtv') {
|
||||
section( "Set your location" ) { input "zipCode", "text", title: "Zip code" }
|
||||
}
|
||||
|
||||
section("Things to check?") {
|
||||
@@ -60,7 +65,11 @@ def scheduleCheck(evt) {
|
||||
// Only need to poll if we haven't checked in a while - and if something is left open.
|
||||
if((now() - (30 * 60 * 1000) > state.lastCheck["time"]) && open) {
|
||||
log.info("Something's open - let's check the weather.")
|
||||
def response = getWeatherFeature("forecast", zipcode)
|
||||
def response
|
||||
if (location.channelName != 'samsungtv')
|
||||
response = getWeatherFeature("forecast", zipCode)
|
||||
else
|
||||
response = getWeatherFeature("forecast")
|
||||
def weather = isStormy(response)
|
||||
|
||||
if(weather) {
|
||||
|
||||
@@ -26,9 +26,9 @@ preferences {
|
||||
}
|
||||
|
||||
section("Temperature monitor?") {
|
||||
input "temp", "capability.temperatureMeasurement", title: "Temp Sensor", required: false
|
||||
input "maxTemp", "number", title: "Max Temp?", required: false
|
||||
input "minTemp", "number", title: "Min Temp?", required: false
|
||||
input "temp", "capability.temperatureMeasurement", title: "Temperature Sensor", required: false
|
||||
input "maxTemp", "number", title: "Max Temperature (°${location.temperatureScale})", required: false
|
||||
input "minTemp", "number", title: "Min Temperature (°${location.temperatureScale})", required: false
|
||||
}
|
||||
|
||||
section("When which people are away?") {
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
* Author: Juan Risso
|
||||
* Date: 2013-12-19
|
||||
*/
|
||||
|
||||
include 'asynchttp_v1'
|
||||
|
||||
definition(
|
||||
name: "Jawbone UP (Connect)",
|
||||
namespace: "juano2310",
|
||||
@@ -84,6 +87,7 @@ def authPage() {
|
||||
log.debug "RedirectURL = ${redirectUrl}"
|
||||
def donebutton= state.JawboneAccessToken != null
|
||||
return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install: donebutton) {
|
||||
section { paragraph title: "Note:", "This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance." }
|
||||
section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", state: hast ,description:description }
|
||||
}
|
||||
} else {
|
||||
@@ -234,7 +238,7 @@ def validateCurrentToken() {
|
||||
httpPost(uri: url, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], body: requestBody) {response ->
|
||||
if (response.status == 200) {
|
||||
log.debug "${response.data}"
|
||||
log.debug "Setting refresh token to ${response.data.data.refresh_token}"
|
||||
log.debug "Setting refresh token"
|
||||
state.refreshToken = response.data.data.refresh_token
|
||||
}
|
||||
}
|
||||
@@ -258,7 +262,7 @@ def validateCurrentToken() {
|
||||
state.remove("refreshToken")
|
||||
}
|
||||
} else {
|
||||
log.debug "Setting access token to ${data.access_token}, refresh token to ${data.refresh_token}"
|
||||
log.debug "Setting access token"
|
||||
state.JawboneAccessToken = data.access_token
|
||||
state.refreshToken = data.refresh_token
|
||||
}
|
||||
@@ -302,7 +306,8 @@ def setup() {
|
||||
def childDevice = addChildDevice('juano2310', "Jawbone User", "${app.id}.${member.xid}",null,[name:"Jawbone UP - " + member.first, completedSetup: true])
|
||||
if (childDevice) {
|
||||
log.debug "Child Device Successfully Created"
|
||||
generateInitialEvent (member, childDevice)
|
||||
childDevice?.generateSleepingEvent(false)
|
||||
pollChild(childDevice)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -348,66 +353,66 @@ def uninstalled() {
|
||||
}
|
||||
|
||||
def pollChild(childDevice) {
|
||||
def member = state.member
|
||||
generatePollingEvents (member, childDevice)
|
||||
def childMap = [ value: "$childDevice.device.deviceNetworkId}"]
|
||||
|
||||
def params = [
|
||||
uri: 'https://jawbone.com',
|
||||
path: '/nudge/api/users/@me/goals',
|
||||
headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ],
|
||||
contentType: 'application/json'
|
||||
]
|
||||
|
||||
asynchttp_v1.get('responseGoals', params, childMap)
|
||||
|
||||
def params2 = [
|
||||
uri: 'https://jawbone.com',
|
||||
path: '/nudge/api/users/@me/moves',
|
||||
headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ],
|
||||
contentType: 'application/json'
|
||||
]
|
||||
|
||||
asynchttp_v1.get('responseMoves', params2, childMap)
|
||||
}
|
||||
|
||||
def generatePollingEvents (member, childDevice) {
|
||||
// lets figure out if the member is currently "home" (At the place)
|
||||
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
|
||||
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
|
||||
def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps"
|
||||
def goals = null
|
||||
def moves = null
|
||||
def sleeps = null
|
||||
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||
goals = response.data.data
|
||||
def responseGoals(response, dni) {
|
||||
if (response.hasError()) {
|
||||
log.error "response has error: $response.errorMessage"
|
||||
} else {
|
||||
def goals
|
||||
try {
|
||||
// json response already parsed into JSONElement object
|
||||
goals = response.json.data
|
||||
} catch (e) {
|
||||
log.error "error parsing json from response: $e"
|
||||
}
|
||||
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||
moves = response.data.data.items[0]
|
||||
}
|
||||
|
||||
try { // we are going to just ignore any errors
|
||||
log.debug "Member = ${member.first}"
|
||||
log.debug "Moves Goal = ${goals.move_steps} Steps"
|
||||
log.debug "Moves = ${moves.details.steps} Steps"
|
||||
|
||||
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
|
||||
if (goals) {
|
||||
def childDevice = getChildDevice(dni.value)
|
||||
log.debug "Goal = ${goals.move_steps} Steps"
|
||||
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
|
||||
//setColor(moves.details.steps,goals.move_steps,childDevice)
|
||||
} else {
|
||||
log.debug "did not get json results from response body: $response.data"
|
||||
}
|
||||
catch (e) {
|
||||
// eat it
|
||||
}
|
||||
}
|
||||
|
||||
def generateInitialEvent (member, childDevice) {
|
||||
// lets figure out if the member is currently "home" (At the place)
|
||||
def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals"
|
||||
def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves"
|
||||
def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps"
|
||||
def goals = null
|
||||
def moves = null
|
||||
def sleeps = null
|
||||
httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||
goals = response.data.data
|
||||
def responseMoves(response, dni) {
|
||||
if (response.hasError()) {
|
||||
log.error "response has error: $response.errorMessage"
|
||||
} else {
|
||||
def moves
|
||||
try {
|
||||
// json response already parsed into JSONElement object
|
||||
moves = response.json.data.items[0]
|
||||
} catch (e) {
|
||||
log.error "error parsing json from response: $e"
|
||||
}
|
||||
httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response ->
|
||||
moves = response.data.data.items[0]
|
||||
}
|
||||
|
||||
try { // we are going to just ignore any errors
|
||||
log.debug "Member = ${member.first}"
|
||||
log.debug "Moves Goal = ${goals.move_steps} Steps"
|
||||
if (moves) {
|
||||
def childDevice = getChildDevice(dni.value)
|
||||
log.debug "Moves = ${moves.details.steps} Steps"
|
||||
log.debug "Sleeping state = false"
|
||||
childDevice?.generateSleepingEvent(false)
|
||||
childDevice?.sendEvent(name:"steps", value: moves.details.steps)
|
||||
childDevice?.sendEvent(name:"goal", value: goals.move_steps)
|
||||
//setColor(moves.details.steps,goals.move_steps,childDevice)
|
||||
} else {
|
||||
log.debug "did not get json results from response body: $response.data"
|
||||
}
|
||||
catch (e) {
|
||||
// eat it
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ def installed() {
|
||||
def updated() {
|
||||
unschedule()
|
||||
turnOffSwitch() //Turn off all switches if the schedules are changed while in mid-schedule
|
||||
unsubscribe
|
||||
unsubscribe()
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
init()
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ definition(
|
||||
name: "Monitor on Sense",
|
||||
namespace: "resteele",
|
||||
author: "Rachel Steele",
|
||||
description: "Turn on Monitor when vibration is sensed",
|
||||
description: "Turn on switch when vibration is sensed",
|
||||
category: "My Apps",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
||||
@@ -25,10 +25,10 @@ definition(
|
||||
|
||||
|
||||
preferences {
|
||||
section("When the keyboard is used...") {
|
||||
section("When vibration is sensed...") {
|
||||
input "accelerationSensor", "capability.accelerationSensor", title: "Which Sensor?"
|
||||
}
|
||||
section("Turn on/off a light...") {
|
||||
section("Turn on switch...") {
|
||||
input "switch1", "capability.switch"
|
||||
}
|
||||
}
|
||||
@@ -47,5 +47,3 @@ def updated() {
|
||||
def accelerationActiveHandler(evt) {
|
||||
switch1.on()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -114,13 +114,16 @@ def beaconHandler(evt) {
|
||||
|
||||
if (allOk) {
|
||||
def data = new groovy.json.JsonSlurper().parseText(evt.data)
|
||||
log.debug "<beacon-control> data: $data - phones: " + phones*.deviceNetworkId
|
||||
// removed logging of device names. can be added back for debugging
|
||||
//log.debug "<beacon-control> data: $data - phones: " + phones*.deviceNetworkId
|
||||
|
||||
def beaconName = getBeaconName(evt)
|
||||
log.debug "<beacon-control> beaconName: $beaconName"
|
||||
// removed logging of device names. can be added back for debugging
|
||||
//log.debug "<beacon-control> beaconName: $beaconName"
|
||||
|
||||
def phoneName = getPhoneName(data)
|
||||
log.debug "<beacon-control> phoneName: $phoneName"
|
||||
// removed logging of device names. can be added back for debugging
|
||||
//log.debug "<beacon-control> phoneName: $phoneName"
|
||||
if (phoneName != null) {
|
||||
def action = data.presence == "1" ? "arrived" : "left"
|
||||
def msg = "$phoneName has $action ${action == 'arrived' ? 'at ' : ''}the $beaconName"
|
||||
|
||||
@@ -49,13 +49,15 @@ preferences {
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
||||
// commented out log statement because presence sensor label could contain user's name
|
||||
//log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
||||
subscribe(people, "presence", presence)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
||||
// commented out log statement because presence sensor label could contain user's name
|
||||
//log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
||||
unsubscribe()
|
||||
subscribe(people, "presence", presence)
|
||||
}
|
||||
|
||||
@@ -116,7 +116,6 @@ def callback() {
|
||||
def oauthState = params.state
|
||||
|
||||
if (oauthState == atomicState.oauthInitState) {
|
||||
|
||||
def tokenParams = [
|
||||
grant_type: "authorization_code",
|
||||
code : code,
|
||||
@@ -129,9 +128,6 @@ def callback() {
|
||||
httpPost(uri: tokenUrl) { resp ->
|
||||
atomicState.refreshToken = resp.data.refresh_token
|
||||
atomicState.authToken = resp.data.access_token
|
||||
log.debug "swapped token: $resp.data"
|
||||
log.debug "atomicState.refreshToken: ${atomicState.refreshToken}"
|
||||
log.debug "atomicState.authToken: ${atomicState.authToken}"
|
||||
}
|
||||
|
||||
if (atomicState.authToken) {
|
||||
@@ -200,7 +196,6 @@ def connectionStatus(message, redirectUrl = null) {
|
||||
.container {
|
||||
width: 90%;
|
||||
padding: 4%;
|
||||
/*background: #eee;*/
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
@@ -237,19 +232,26 @@ def getEcobeeThermostats() {
|
||||
log.debug "getting device list"
|
||||
atomicState.remoteSensors = []
|
||||
|
||||
def requestBody = '{"selection":{"selectionType":"registered","selectionMatch":"","includeRuntime":true,"includeSensors":true}}'
|
||||
|
||||
def bodyParams = [
|
||||
selection: [
|
||||
selectionType: "registered",
|
||||
selectionMatch: "",
|
||||
includeRuntime: true,
|
||||
includeSensors: true
|
||||
]
|
||||
]
|
||||
def deviceListParams = [
|
||||
uri: apiEndpoint,
|
||||
path: "/1/thermostat",
|
||||
headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
||||
query: [format: 'json', body: requestBody]
|
||||
// TODO - the query string below is not consistent with the Ecobee docs:
|
||||
// https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml
|
||||
query: [format: 'json', body: toJson(bodyParams)]
|
||||
]
|
||||
|
||||
def stats = [:]
|
||||
try {
|
||||
httpGet(deviceListParams) { resp ->
|
||||
|
||||
if (resp.status == 200) {
|
||||
resp.data.thermostatList.each { stat ->
|
||||
atomicState.remoteSensors = atomicState.remoteSensors == null ? stat.remoteSensors : atomicState.remoteSensors << stat.remoteSensors
|
||||
@@ -289,8 +291,9 @@ Map sensorsDiscovered() {
|
||||
}
|
||||
|
||||
def getThermostatDisplayName(stat) {
|
||||
if(stat?.name)
|
||||
if(stat?.name) {
|
||||
return stat.name.toString()
|
||||
}
|
||||
return (getThermostatTypeName(stat) + " (${stat.identifier})").toString()
|
||||
}
|
||||
|
||||
@@ -310,7 +313,6 @@ def updated() {
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
|
||||
log.debug "initialize"
|
||||
def devices = thermostats.collect { dni ->
|
||||
def d = getChildDevice(dni)
|
||||
@@ -350,8 +352,6 @@ def initialize() {
|
||||
log.warn "delete: ${delete}, deleting ${delete.size()} thermostats"
|
||||
delete.each { deleteChildDevice(it.deviceNetworkId) } //inherits from SmartApp (data-management)
|
||||
|
||||
atomicState.thermostatData = [:] //reset Map to store thermostat data
|
||||
|
||||
//send activity feeds to tell that device is connected
|
||||
def notificationMessage = "is connected to SmartThings"
|
||||
sendActivityFeeds(notificationMessage)
|
||||
@@ -383,16 +383,27 @@ def pollHandler() {
|
||||
def pollChildren(child = null) {
|
||||
def thermostatIdsString = getChildDeviceIdsString()
|
||||
log.debug "polling children: $thermostatIdsString"
|
||||
def data = ""
|
||||
|
||||
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeExtendedRuntime":"true","includeSettings":"true","includeRuntime":"true","includeSensors":true}}'
|
||||
def requestBody = [
|
||||
selection: [
|
||||
selectionType: "thermostats",
|
||||
selectionMatch: thermostatIdsString,
|
||||
includeExtendedRuntime: true,
|
||||
includeSettings: true,
|
||||
includeRuntime: true,
|
||||
includeSensors: true
|
||||
]
|
||||
]
|
||||
|
||||
def result = false
|
||||
|
||||
def pollParams = [
|
||||
uri: apiEndpoint,
|
||||
path: "/1/thermostat",
|
||||
headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
||||
query: [format: 'json', body: jsonRequestBody]
|
||||
// TODO - the query string below is not consistent with the Ecobee docs:
|
||||
// https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml
|
||||
query: [format: 'json', body: toJson(requestBody)]
|
||||
]
|
||||
|
||||
try{
|
||||
@@ -400,53 +411,8 @@ def pollChildren(child = null) {
|
||||
if(resp.status == 200) {
|
||||
log.debug "poll results returned resp.data ${resp.data}"
|
||||
atomicState.remoteSensors = resp.data.thermostatList.remoteSensors
|
||||
atomicState.thermostatData = resp.data
|
||||
updateSensorData()
|
||||
atomicState.thermostats = resp.data.thermostatList.inject([:]) { collector, stat ->
|
||||
def dni = [ app.id, stat.identifier ].join('.')
|
||||
|
||||
log.debug "updating dni $dni"
|
||||
|
||||
data = [
|
||||
coolMode: (stat.settings.coolStages > 0),
|
||||
heatMode: (stat.settings.heatStages > 0),
|
||||
deviceTemperatureUnit: stat.settings.useCelsius,
|
||||
minHeatingSetpoint: (stat.settings.heatRangeLow / 10),
|
||||
maxHeatingSetpoint: (stat.settings.heatRangeHigh / 10),
|
||||
minCoolingSetpoint: (stat.settings.coolRangeLow / 10),
|
||||
maxCoolingSetpoint: (stat.settings.coolRangeHigh / 10),
|
||||
autoMode: stat.settings.autoHeatCoolFeatureEnabled,
|
||||
auxHeatMode: (stat.settings.hasHeatPump) && (stat.settings.hasForcedAir || stat.settings.hasElectric || stat.settings.hasBoiler),
|
||||
temperature: (stat.runtime.actualTemperature / 10),
|
||||
heatingSetpoint: stat.runtime.desiredHeat / 10,
|
||||
coolingSetpoint: stat.runtime.desiredCool / 10,
|
||||
thermostatMode: stat.settings.hvacMode,
|
||||
humidity: stat.runtime.actualHumidity,
|
||||
thermostatFanMode: stat.runtime.desiredFanMode
|
||||
]
|
||||
|
||||
if (location.temperatureScale == "F")
|
||||
{
|
||||
data["temperature"] = data["temperature"] ? Math.round(data["temperature"].toDouble()) : data["temperature"]
|
||||
data["heatingSetpoint"] = data["heatingSetpoint"] ? Math.round(data["heatingSetpoint"].toDouble()) : data["heatingSetpoint"]
|
||||
data["coolingSetpoint"] = data["coolingSetpoint"] ? Math.round(data["coolingSetpoint"].toDouble()) : data["coolingSetpoint"]
|
||||
data["minHeatingSetpoint"] = data["minHeatingSetpoint"] ? Math.round(data["minHeatingSetpoint"].toDouble()) : data["minHeatingSetpoint"]
|
||||
data["maxHeatingSetpoint"] = data["maxHeatingSetpoint"] ? Math.round(data["maxHeatingSetpoint"].toDouble()) : data["maxHeatingSetpoint"]
|
||||
data["minCoolingSetpoint"] = data["minCoolingSetpoint"] ? Math.round(data["minCoolingSetpoint"].toDouble()) : data["minCoolingSetpoint"]
|
||||
data["maxCoolingSetpoint"] = data["maxCoolingSetpoint"] ? Math.round(data["maxCoolingSetpoint"].toDouble()) : data["maxCoolingSetpoint"]
|
||||
|
||||
}
|
||||
|
||||
if (data?.deviceTemperatureUnit == false && location.temperatureScale == "F") {
|
||||
data["deviceTemperatureUnit"] = "F"
|
||||
|
||||
} else {
|
||||
data["deviceTemperatureUnit"] = "C"
|
||||
}
|
||||
|
||||
collector[dni] = [data:data]
|
||||
return collector
|
||||
}
|
||||
storeThermostatData(resp.data.thermostatList)
|
||||
result = true
|
||||
log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}"
|
||||
}
|
||||
@@ -464,7 +430,6 @@ def pollChildren(child = null) {
|
||||
|
||||
// Poll Child is invoked from the Child Device itself as part of the Poll Capability
|
||||
def pollChild() {
|
||||
|
||||
def devices = getChildDevices()
|
||||
|
||||
if (pollChildren()) {
|
||||
@@ -492,36 +457,7 @@ void poll() {
|
||||
}
|
||||
|
||||
def availableModes(child) {
|
||||
|
||||
debugEvent ("atomicState.thermostats = ${atomicState.thermostats}")
|
||||
|
||||
debugEvent ("Child DNI = ${child.device.deviceNetworkId}")
|
||||
|
||||
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
||||
|
||||
debugEvent("Data = ${tData}")
|
||||
|
||||
if(!tData)
|
||||
{
|
||||
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling"
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
def modes = ["off"]
|
||||
|
||||
if (tData.data.heatMode) modes.add("heat")
|
||||
if (tData.data.coolMode) modes.add("cool")
|
||||
if (tData.data.autoMode) modes.add("auto")
|
||||
if (tData.data.auxHeatMode) modes.add("auxHeatOnly")
|
||||
|
||||
modes
|
||||
|
||||
}
|
||||
|
||||
def currentMode(child) {
|
||||
debugEvent ("atomicState.Thermos = ${atomicState.thermostats}")
|
||||
|
||||
debugEvent ("Child DNI = ${child.device.deviceNetworkId}")
|
||||
|
||||
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
||||
@@ -530,14 +466,42 @@ def currentMode(child) {
|
||||
|
||||
if(!tData) {
|
||||
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling"
|
||||
return null
|
||||
}
|
||||
|
||||
def modes = ["off"]
|
||||
|
||||
if (tData.data.heatMode) {
|
||||
modes.add("heat")
|
||||
}
|
||||
if (tData.data.coolMode) {
|
||||
modes.add("cool")
|
||||
}
|
||||
if (tData.data.autoMode) {
|
||||
modes.add("auto")
|
||||
}
|
||||
if (tData.data.auxHeatMode) {
|
||||
modes.add("auxHeatOnly")
|
||||
}
|
||||
|
||||
return modes
|
||||
}
|
||||
|
||||
def currentMode(child) {
|
||||
debugEvent ("atomicState.Thermos = ${atomicState.thermostats}")
|
||||
debugEvent ("Child DNI = ${child.device.deviceNetworkId}")
|
||||
|
||||
def tData = atomicState.thermostats[child.device.deviceNetworkId]
|
||||
|
||||
debugEvent("Data = ${tData}")
|
||||
|
||||
if(!tData) {
|
||||
log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling"
|
||||
return null
|
||||
}
|
||||
|
||||
def mode = tData.data.thermostatMode
|
||||
|
||||
mode
|
||||
return mode
|
||||
}
|
||||
|
||||
def updateSensorData() {
|
||||
@@ -558,12 +522,12 @@ def updateSensorData() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else if (it.type == "occupancy") {
|
||||
if(it.value == "true")
|
||||
if(it.value == "true") {
|
||||
occupancy = "active"
|
||||
else
|
||||
} else {
|
||||
occupancy = "inactive"
|
||||
}
|
||||
}
|
||||
}
|
||||
def dni = "ecobee_sensor-"+ it?.id + "-" + it?.code
|
||||
@@ -582,7 +546,7 @@ def getChildDeviceIdsString() {
|
||||
}
|
||||
|
||||
def toJson(Map m) {
|
||||
return new org.json.JSONObject(m).toString()
|
||||
return groovy.json.JsonOutput.toJson(m)
|
||||
}
|
||||
|
||||
def toQueryString(Map m) {
|
||||
@@ -595,7 +559,6 @@ private refreshAuthToken() {
|
||||
if(!atomicState.refreshToken) {
|
||||
log.warn "Can not refresh OAuth token since there is no refreshToken stored"
|
||||
} else {
|
||||
|
||||
def refreshParams = [
|
||||
method: 'POST',
|
||||
uri : apiEndpoint,
|
||||
@@ -603,44 +566,15 @@ private refreshAuthToken() {
|
||||
query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: smartThingsClientId],
|
||||
]
|
||||
|
||||
log.debug refreshParams
|
||||
|
||||
def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials."
|
||||
//changed to httpPost
|
||||
try {
|
||||
def jsonMap
|
||||
httpPost(refreshParams) { resp ->
|
||||
|
||||
if(resp.status == 200) {
|
||||
log.debug "Token refreshed...calling saved RestAction now!"
|
||||
|
||||
debugEvent("Token refreshed ... calling saved RestAction now!")
|
||||
|
||||
log.debug resp
|
||||
|
||||
jsonMap = resp.data
|
||||
|
||||
if(resp.data) {
|
||||
|
||||
log.debug resp.data
|
||||
debugEvent("Response = ${resp.data}")
|
||||
|
||||
atomicState.refreshToken = resp?.data?.refresh_token
|
||||
atomicState.authToken = resp?.data?.access_token
|
||||
|
||||
debugEvent("Refresh Token = ${atomicState.refreshToken}")
|
||||
debugEvent("OAUTH Token = ${atomicState.authToken}")
|
||||
|
||||
if(atomicState.action && atomicState.action != "") {
|
||||
log.debug "Executing next action: ${atomicState.action}"
|
||||
|
||||
"${atomicState.action}"()
|
||||
|
||||
atomicState.action = ""
|
||||
}
|
||||
|
||||
}
|
||||
atomicState.action = ""
|
||||
saveTokenAndResumeAction(resp.data)
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
@@ -662,64 +596,174 @@ private refreshAuthToken() {
|
||||
}
|
||||
}
|
||||
|
||||
def resumeProgram(child, deviceId) {
|
||||
|
||||
|
||||
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}]}'
|
||||
def result = sendJson(jsonRequestBody)
|
||||
return result
|
||||
/**
|
||||
* Saves the refresh and auth token from the passed-in JSON object,
|
||||
* and invokes any previously executing action that did not complete due to
|
||||
* an expired token.
|
||||
*
|
||||
* @param json - an object representing the parsed JSON response from Ecobee
|
||||
*/
|
||||
private void saveTokenAndResumeAction(json) {
|
||||
log.debug "token response json: $json"
|
||||
if (json) {
|
||||
debugEvent("Response = $json")
|
||||
atomicState.refreshToken = json?.refresh_token
|
||||
atomicState.authToken = json?.access_token
|
||||
if (atomicState.action) {
|
||||
log.debug "got refresh token, executing next action: ${atomicState.action}"
|
||||
"${atomicState.action}"()
|
||||
}
|
||||
} else {
|
||||
log.warn "did not get response body from refresh token response"
|
||||
}
|
||||
atomicState.action = ""
|
||||
}
|
||||
|
||||
def setHold(child, heating, cooling, deviceId, sendHoldType) {
|
||||
|
||||
int h = heating * 10
|
||||
int c = cooling * 10
|
||||
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": '+sendHoldType+' } } ]}'
|
||||
|
||||
def result = sendJson(child, jsonRequestBody)
|
||||
return result
|
||||
/**
|
||||
* Executes the resume program command on the Ecobee thermostat
|
||||
* @param deviceId - the ID of the device
|
||||
*
|
||||
* @retrun true if the command was successful, false otherwise.
|
||||
*/
|
||||
boolean resumeProgram(deviceId) {
|
||||
def payload = [
|
||||
selection: [
|
||||
selectionType: "thermostats",
|
||||
selectionMatch: deviceId,
|
||||
includeRuntime: true
|
||||
],
|
||||
functions: [
|
||||
[
|
||||
type: "resumeProgram"
|
||||
]
|
||||
]
|
||||
]
|
||||
return sendCommandToEcobee(payload)
|
||||
}
|
||||
|
||||
def setFanMode(child, heating, cooling, deviceId, sendHoldType, fanMode) {
|
||||
|
||||
/**
|
||||
* Executes the set hold command on the Ecobee thermostat
|
||||
* @param heating - The heating temperature to set in fahrenheit
|
||||
* @param cooling - the cooling temperature to set in fahrenheit
|
||||
* @param deviceId - the ID of the device
|
||||
* @param sendHoldType - the hold type to execute
|
||||
*
|
||||
* @return true if the command was successful, false otherwise
|
||||
*/
|
||||
boolean setHold(heating, cooling, deviceId, sendHoldType) {
|
||||
// Ecobee requires that temp values be in fahrenheit multiplied by 10.
|
||||
int h = heating * 10
|
||||
int c = cooling * 10
|
||||
|
||||
def payload = [
|
||||
selection: [
|
||||
selectionType: "thermostats",
|
||||
selectionMatch: deviceId,
|
||||
includeRuntime: true
|
||||
],
|
||||
functions: [
|
||||
[
|
||||
type: "setHold",
|
||||
params: [
|
||||
coolHoldTemp: c,
|
||||
heatHoldTemp: h,
|
||||
holdType: sendHoldType
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": '+sendHoldType+', "fan": '+fanMode+' } } ]}'
|
||||
def result = sendJson(child, jsonRequestBody)
|
||||
return result
|
||||
return sendCommandToEcobee(payload)
|
||||
}
|
||||
|
||||
def setMode(child, mode, deviceId) {
|
||||
def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + deviceId + '","includeRuntime":true},"thermostat": {"settings":{"hvacMode":"'+"${mode}"+'"}}}'
|
||||
/**
|
||||
* Executes the set fan mode command on the Ecobee thermostat
|
||||
* @param heating - The heating temperature to set in fahrenheit
|
||||
* @param cooling - the cooling temperature to set in fahrenheit
|
||||
* @param deviceId - the ID of the device
|
||||
* @param sendHoldType - the hold type to execute
|
||||
* @param fanMode - the fan mode to set to
|
||||
*
|
||||
* @return true if the command was successful, false otherwise
|
||||
*/
|
||||
boolean setFanMode(heating, cooling, deviceId, sendHoldType, fanMode) {
|
||||
// Ecobee requires that temp values be in fahrenheit multiplied by 10.
|
||||
int h = heating * 10
|
||||
int c = cooling * 10
|
||||
|
||||
def result = sendJson(jsonRequestBody)
|
||||
return result
|
||||
def payload = [
|
||||
selection: [
|
||||
selectionType: "thermostats",
|
||||
selectionMatch: deviceId,
|
||||
includeRuntime: true
|
||||
],
|
||||
functions: [
|
||||
[
|
||||
type: "setHold",
|
||||
params: [
|
||||
coolHoldTemp: c,
|
||||
heatHoldTemp: h,
|
||||
holdType: sendHoldType,
|
||||
fan: fanMode
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
return sendCommandToEcobee(payload)
|
||||
}
|
||||
|
||||
def sendJson(child = null, String jsonBody) {
|
||||
/**
|
||||
* Sets the mode of the Ecobee thermostat
|
||||
* @param mode - the mode to set to
|
||||
* @param deviceId - the ID of the device
|
||||
*
|
||||
* @return true if the command was successful, false otherwise
|
||||
*/
|
||||
boolean setMode(mode, deviceId) {
|
||||
def payload = [
|
||||
selection: [
|
||||
selectionType: "thermostats",
|
||||
selectionMatch: deviceId,
|
||||
includeRuntime: true
|
||||
],
|
||||
thermostat: [
|
||||
settings: [
|
||||
hvacMode: mode
|
||||
]
|
||||
]
|
||||
]
|
||||
return sendCommandToEcobee(payload)
|
||||
}
|
||||
|
||||
def returnStatus = false
|
||||
/**
|
||||
* Makes a request to the Ecobee API to actuate the thermostat.
|
||||
* Used by command methods to send commands to Ecobee.
|
||||
*
|
||||
* @param bodyParams - a map of request parameters to send to Ecobee.
|
||||
*
|
||||
* @return true if the command was accepted by Ecobee without error, false otherwise.
|
||||
*/
|
||||
private boolean sendCommandToEcobee(Map bodyParams) {
|
||||
def isSuccess = false
|
||||
def cmdParams = [
|
||||
uri: apiEndpoint,
|
||||
path: "/1/thermostat",
|
||||
headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
||||
body: jsonBody
|
||||
body: toJson(bodyParams)
|
||||
]
|
||||
|
||||
try{
|
||||
httpPost(cmdParams) { resp ->
|
||||
|
||||
if(resp.status == 200) {
|
||||
|
||||
log.debug "updated ${resp.data}"
|
||||
returnStatus = resp.data.status.code
|
||||
if (resp.data.status.code == 0)
|
||||
def returnStatus = resp.data.status.code
|
||||
if (returnStatus == 0) {
|
||||
log.debug "Successful call to ecobee API."
|
||||
else {
|
||||
log.debug "Error return code = ${resp.data.status.code}"
|
||||
debugEvent("Error return code = ${resp.data.status.code}")
|
||||
isSuccess = true
|
||||
} else {
|
||||
log.debug "Error return code = ${returnStatus}"
|
||||
debugEvent("Error return code = ${returnStatus}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -727,33 +771,30 @@ def sendJson(child = null, String jsonBody) {
|
||||
log.trace "Exception Sending Json: " + e.response.data.status
|
||||
debugEvent ("sent Json & got http status ${e.statusCode} - ${e.response.data.status.code}")
|
||||
if (e.response.data.status.code == 14) {
|
||||
// TODO - figure out why we're setting the next action to be pollChildren
|
||||
// after refreshing auth token. Is it to keep UI in sync, or just copy/paste error?
|
||||
atomicState.action = "pollChildren"
|
||||
log.debug "Refreshing your auth_token!"
|
||||
refreshAuthToken()
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
debugEvent("Authentication error, invalid authentication method, lack of credentials, etc.")
|
||||
log.error "Authentication error, invalid authentication method, lack of credentials, etc."
|
||||
}
|
||||
}
|
||||
|
||||
if (returnStatus == 0)
|
||||
return true
|
||||
else
|
||||
return false
|
||||
return isSuccess
|
||||
}
|
||||
|
||||
def getChildName() { "Ecobee Thermostat" }
|
||||
def getSensorChildName() { "Ecobee Sensor" }
|
||||
def getChildName() { return "Ecobee Thermostat" }
|
||||
def getSensorChildName() { return "Ecobee Sensor" }
|
||||
def getServerUrl() { return "https://graph.api.smartthings.com" }
|
||||
def getShardUrl() { return getApiServerUrl() }
|
||||
def getCallbackUrl() { "https://graph.api.smartthings.com/oauth/callback" }
|
||||
def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${shardUrl}" }
|
||||
def getApiEndpoint() { "https://api.ecobee.com" }
|
||||
def getSmartThingsClientId() { appSettings.clientId }
|
||||
def getCallbackUrl() { return "https://graph.api.smartthings.com/oauth/callback" }
|
||||
def getBuildRedirectUrl() { return "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${shardUrl}" }
|
||||
def getApiEndpoint() { return "https://api.ecobee.com" }
|
||||
def getSmartThingsClientId() { return appSettings.clientId }
|
||||
|
||||
def debugEvent(message, displayEvent = false) {
|
||||
|
||||
def results = [
|
||||
name: "appdebug",
|
||||
descriptionText: message,
|
||||
@@ -761,11 +802,6 @@ def debugEvent(message, displayEvent = false) {
|
||||
]
|
||||
log.debug "Generating AppDebug Event: ${results}"
|
||||
sendEvent (results)
|
||||
|
||||
}
|
||||
|
||||
def debugEventFromParent(child, message) {
|
||||
if (child != null) { child.sendEvent("name":"debugEventFromParent", "value":message, "description":message, displayed: true, isStateChange: true)}
|
||||
}
|
||||
|
||||
//send both push notification and mobile activity feeds
|
||||
@@ -786,6 +822,58 @@ def sendPushAndFeeds(notificationMessage){
|
||||
atomicState.authToken = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores data about the thermostats in atomicState.
|
||||
* @param thermostats - a list of thermostats as returned from the Ecobee API
|
||||
*/
|
||||
private void storeThermostatData(thermostats) {
|
||||
log.trace "Storing thermostat data: $thermostats"
|
||||
def data
|
||||
atomicState.thermostats = thermostats.inject([:]) { collector, stat ->
|
||||
def dni = [ app.id, stat.identifier ].join('.')
|
||||
log.debug "updating dni $dni"
|
||||
|
||||
data = [
|
||||
coolMode: (stat.settings.coolStages > 0),
|
||||
heatMode: (stat.settings.heatStages > 0),
|
||||
deviceTemperatureUnit: stat.settings.useCelsius,
|
||||
minHeatingSetpoint: (stat.settings.heatRangeLow / 10),
|
||||
maxHeatingSetpoint: (stat.settings.heatRangeHigh / 10),
|
||||
minCoolingSetpoint: (stat.settings.coolRangeLow / 10),
|
||||
maxCoolingSetpoint: (stat.settings.coolRangeHigh / 10),
|
||||
autoMode: stat.settings.autoHeatCoolFeatureEnabled,
|
||||
auxHeatMode: (stat.settings.hasHeatPump) && (stat.settings.hasForcedAir || stat.settings.hasElectric || stat.settings.hasBoiler),
|
||||
temperature: (stat.runtime.actualTemperature / 10),
|
||||
heatingSetpoint: stat.runtime.desiredHeat / 10,
|
||||
coolingSetpoint: stat.runtime.desiredCool / 10,
|
||||
thermostatMode: stat.settings.hvacMode,
|
||||
humidity: stat.runtime.actualHumidity,
|
||||
thermostatFanMode: stat.runtime.desiredFanMode
|
||||
]
|
||||
if (location.temperatureScale == "F") {
|
||||
data["temperature"] = data["temperature"] ? Math.round(data["temperature"].toDouble()) : data["temperature"]
|
||||
data["heatingSetpoint"] = data["heatingSetpoint"] ? Math.round(data["heatingSetpoint"].toDouble()) : data["heatingSetpoint"]
|
||||
data["coolingSetpoint"] = data["coolingSetpoint"] ? Math.round(data["coolingSetpoint"].toDouble()) : data["coolingSetpoint"]
|
||||
data["minHeatingSetpoint"] = data["minHeatingSetpoint"] ? Math.round(data["minHeatingSetpoint"].toDouble()) : data["minHeatingSetpoint"]
|
||||
data["maxHeatingSetpoint"] = data["maxHeatingSetpoint"] ? Math.round(data["maxHeatingSetpoint"].toDouble()) : data["maxHeatingSetpoint"]
|
||||
data["minCoolingSetpoint"] = data["minCoolingSetpoint"] ? Math.round(data["minCoolingSetpoint"].toDouble()) : data["minCoolingSetpoint"]
|
||||
data["maxCoolingSetpoint"] = data["maxCoolingSetpoint"] ? Math.round(data["maxCoolingSetpoint"].toDouble()) : data["maxCoolingSetpoint"]
|
||||
|
||||
}
|
||||
|
||||
if (data?.deviceTemperatureUnit == false && location.temperatureScale == "F") {
|
||||
data["deviceTemperatureUnit"] = "F"
|
||||
|
||||
} else {
|
||||
data["deviceTemperatureUnit"] = "C"
|
||||
}
|
||||
|
||||
collector[dni] = [data:data]
|
||||
return collector
|
||||
}
|
||||
log.debug "updated ${atomicState.thermostats?.size()} thermostats: ${atomicState.thermostats}"
|
||||
}
|
||||
|
||||
def sendActivityFeeds(notificationMessage) {
|
||||
def devices = getChildDevices()
|
||||
devices.each { child ->
|
||||
@@ -793,14 +881,6 @@ def sendActivityFeeds(notificationMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
def roundC (tempC) {
|
||||
return String.format("%.1f", (Math.round(tempC * 2))/2)
|
||||
}
|
||||
|
||||
def convertFtoC (tempF) {
|
||||
return String.format("%.1f", (Math.round(((tempF - 32)*(5/9)) * 2))/2)
|
||||
}
|
||||
|
||||
def convertCtoF (tempC) {
|
||||
return (Math.round(tempC * (9/5)) + 32).toInteger()
|
||||
}
|
||||
|
||||
@@ -64,10 +64,12 @@ def meterHandler(evt) {
|
||||
def lastValue = atomicState.lastValue as double
|
||||
atomicState.lastValue = meterValue
|
||||
|
||||
def dUnit = evt.unit ?: "Watts"
|
||||
|
||||
def aboveThresholdValue = aboveThreshold as int
|
||||
if (meterValue > aboveThresholdValue) {
|
||||
if (lastValue < aboveThresholdValue) { // only send notifications when crossing the threshold
|
||||
def msg = "${meter} reported ${evt.value} ${evt.unit} which is above your threshold of ${aboveThreshold}."
|
||||
def msg = "${meter} reported ${evt.value} ${dUnit} which is above your threshold of ${aboveThreshold}."
|
||||
sendMessage(msg)
|
||||
} else {
|
||||
// log.debug "not sending notification for ${evt.description} because the threshold (${aboveThreshold}) has already been crossed"
|
||||
@@ -78,7 +80,7 @@ def meterHandler(evt) {
|
||||
def belowThresholdValue = belowThreshold as int
|
||||
if (meterValue < belowThresholdValue) {
|
||||
if (lastValue > belowThresholdValue) { // only send notifications when crossing the threshold
|
||||
def msg = "${meter} reported ${evt.value} ${evt.unit} which is below your threshold of ${belowThreshold}."
|
||||
def msg = "${meter} reported ${evt.value} ${dUnit} which is below your threshold of ${belowThreshold}."
|
||||
sendMessage(msg)
|
||||
} else {
|
||||
// log.debug "not sending notification for ${evt.description} because the threshold (${belowThreshold}) has already been crossed"
|
||||
|
||||
@@ -54,10 +54,10 @@ def waterWetHandler(evt) {
|
||||
def alreadySentSms = recentEvents.count { it.value && it.value == "wet" } > 1
|
||||
|
||||
if (alreadySentSms) {
|
||||
log.debug "SMS already sent to $phone within the last $deltaSeconds seconds"
|
||||
log.debug "SMS already sent within the last $deltaSeconds seconds"
|
||||
} else {
|
||||
def msg = "${alarm.displayName} is wet!"
|
||||
log.debug "$alarm is wet, texting $phone"
|
||||
log.debug "$alarm is wet, texting phone number"
|
||||
|
||||
if (location.contactBookEnabled) {
|
||||
sendNotificationToContacts(msg, recipients)
|
||||
|
||||
@@ -90,7 +90,7 @@ def takeAction(){
|
||||
}
|
||||
|
||||
def sendTextMessage() {
|
||||
log.debug "$multisensor was open too long, texting $phone"
|
||||
log.debug "$multisensor was open too long, texting phone"
|
||||
|
||||
updateSmsHistory()
|
||||
def openMinutes = maxOpenTime * (state.smsHistory?.size() ?: 1)
|
||||
|
||||
@@ -454,17 +454,23 @@ def sendStopEvent(source) {
|
||||
eventData.value += "cancelled"
|
||||
}
|
||||
|
||||
// send 100% completion event
|
||||
sendTimeRemainingEvent(100)
|
||||
|
||||
// send a non-displayed 0% completion to reset tiles
|
||||
sendTimeRemainingEvent(0, false)
|
||||
|
||||
// send sessionStatus event last so the event feed is ordered properly
|
||||
sendControllerEvent(eventData)
|
||||
sendTimeRemainingEvent(0)
|
||||
}
|
||||
|
||||
def sendTimeRemainingEvent(percentComplete) {
|
||||
def sendTimeRemainingEvent(percentComplete, displayed = true) {
|
||||
log.trace "sendTimeRemainingEvent(${percentComplete})"
|
||||
|
||||
def percentCompleteEventData = [
|
||||
name: "percentComplete",
|
||||
value: percentComplete as int,
|
||||
displayed: true,
|
||||
displayed: displayed,
|
||||
isStateChange: true
|
||||
]
|
||||
sendControllerEvent(percentCompleteEventData)
|
||||
@@ -474,7 +480,7 @@ def sendTimeRemainingEvent(percentComplete) {
|
||||
def timeRemainingEventData = [
|
||||
name: "timeRemaining",
|
||||
value: displayableTime(timeRemaining),
|
||||
displayed: true,
|
||||
displayed: displayed,
|
||||
isStateChange: true
|
||||
]
|
||||
sendControllerEvent(timeRemainingEventData)
|
||||
@@ -608,8 +614,6 @@ private completion() {
|
||||
handleCompletionMessaging()
|
||||
|
||||
handleCompletionModesAndPhrases()
|
||||
|
||||
sendTimeRemainingEvent(100)
|
||||
}
|
||||
|
||||
private handleCompletionSwitches() {
|
||||
@@ -761,7 +765,7 @@ String displayableTime(timeRemaining) {
|
||||
return "${minutes}:00"
|
||||
}
|
||||
def fraction = "0.${parts[1]}" as double
|
||||
def seconds = "${60 * fraction as int}".padRight(2, "0")
|
||||
def seconds = "${60 * fraction as int}".padLeft(2, "0")
|
||||
return "${minutes}:${seconds}"
|
||||
}
|
||||
|
||||
|
||||
@@ -47,13 +47,13 @@ preferences {
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
||||
// log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
||||
subscribe(people, "presence", presence)
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
||||
// log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}"
|
||||
unsubscribe()
|
||||
subscribe(people, "presence", presence)
|
||||
}
|
||||
@@ -71,11 +71,10 @@ def presence(evt)
|
||||
def person = getPerson(evt)
|
||||
def recentNotPresent = person.statesSince("presence", t0).find{it.value == "not present"}
|
||||
if (recentNotPresent) {
|
||||
log.debug "skipping notification of arrival of ${person.displayName} because last departure was only ${now() - recentNotPresent.date.time} msec ago"
|
||||
log.debug "skipping notification of arrival of Person because last departure was only ${now() - recentNotPresent.date.time} msec ago"
|
||||
}
|
||||
else {
|
||||
def message = "${person.displayName} arrived at home, changing mode to '${newMode}'"
|
||||
log.info message
|
||||
send(message)
|
||||
setLocationMode(newMode)
|
||||
}
|
||||
@@ -106,6 +105,4 @@ private send(msg) {
|
||||
sendSms(phone, msg)
|
||||
}
|
||||
}
|
||||
|
||||
log.debug msg
|
||||
}
|
||||
|
||||
@@ -57,12 +57,11 @@ def scheduleCheck()
|
||||
def message = message1 ?: "SmartThings - Habit Helper Reminder!"
|
||||
|
||||
if (location.contactBookEnabled) {
|
||||
log.debug "Texting reminder: ($message) to contacts:${recipients?.size()}"
|
||||
log.debug "Texting reminder to contacts:${recipients?.size()}"
|
||||
sendNotificationToContacts(message, recipients)
|
||||
}
|
||||
else {
|
||||
|
||||
log.debug "Texting reminder: ($message) to $phone1"
|
||||
log.debug "Texting reminder"
|
||||
sendSms(phone1, message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,10 @@ preferences {
|
||||
|
||||
def mainPage() {
|
||||
def bridges = bridgesDiscovered()
|
||||
if (state.username && bridges) {
|
||||
|
||||
if (state.refreshUsernameNeeded) {
|
||||
return bridgeLinking()
|
||||
} else if (state.username && bridges) {
|
||||
return bulbDiscovery()
|
||||
} else {
|
||||
return bridgeDiscovery()
|
||||
@@ -83,7 +86,7 @@ def bridgeDiscovery(params=[:])
|
||||
|
||||
return dynamicPage(name:"bridgeDiscovery", title:"Discovery Started!", nextPage:"bridgeBtnPush", refreshInterval:refreshInterval, uninstall: true) {
|
||||
section("Please wait while we discover your Hue Bridge. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||
input "selectedHue", "enum", required:false, title:"Select Hue Bridge (${numFound} found)", multiple:false, options:options
|
||||
input "selectedHue", "enum", required:false, title:"Select Hue Bridge (${numFound} found)", multiple:false, options:options, submitOnChange: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,8 +98,7 @@ def bridgeDiscoveryFailed() {
|
||||
}
|
||||
}
|
||||
|
||||
def bridgeLinking()
|
||||
{
|
||||
def bridgeLinking() {
|
||||
int linkRefreshcount = !state.linkRefreshcount ? 0 : state.linkRefreshcount as int
|
||||
state.linkRefreshcount = linkRefreshcount + 1
|
||||
def refreshInterval = 3
|
||||
@@ -105,11 +107,20 @@ def bridgeLinking()
|
||||
def title = "Linking with your Hue"
|
||||
def paragraphText
|
||||
if (selectedHue) {
|
||||
if (state.refreshUsernameNeeded) {
|
||||
paragraphText = "The current Hue username is invalid.\n\nPlease press the button on your Hue Bridge to re-link. "
|
||||
} else {
|
||||
paragraphText = "Press the button on your Hue Bridge to setup a link. "
|
||||
}
|
||||
} else {
|
||||
paragraphText = "You haven't selected a Hue Bridge, please Press \"Done\" and select one before clicking next."
|
||||
}
|
||||
if (state.username) { //if discovery worked
|
||||
if (state.refreshUsernameNeeded) {
|
||||
state.refreshUsernameNeeded = false
|
||||
// Issue one poll with new username to cancel local polling with old username
|
||||
poll()
|
||||
}
|
||||
nextPage = "bulbDiscovery"
|
||||
title = "Success!"
|
||||
paragraphText = "Linking to your hub was a success! Please click 'Next'!"
|
||||
@@ -132,10 +143,7 @@ def bulbDiscovery() {
|
||||
def refreshInterval = 3
|
||||
state.inBulbDiscovery = true
|
||||
def bridge = null
|
||||
if (selectedHue) {
|
||||
bridge = getChildDevice(selectedHue)
|
||||
subscribe(bridge, "bulbList", bulbListData)
|
||||
}
|
||||
|
||||
state.bridgeRefreshCount = 0
|
||||
def allLightsFound = bulbsDiscovered() ?: [:]
|
||||
|
||||
@@ -171,7 +179,7 @@ def bulbDiscovery() {
|
||||
|
||||
return dynamicPage(name:"bulbDiscovery", title:"Light Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
||||
section("Please wait while we discover your Hue Lights. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||
input "selectedBulbs", "enum", required:false, title:"Select Hue Lights to add (${numFound} found)", multiple:true, options:newLights
|
||||
input "selectedBulbs", "enum", required:false, title:"Select Hue Lights to add (${numFound} found)", multiple:true, submitOnChange: true, options:newLights
|
||||
paragraph title: "Previously added Hue Lights (${existingLights.size()} added)", existingLightsDescription
|
||||
}
|
||||
section {
|
||||
@@ -260,10 +268,6 @@ Map bulbsDiscovered() {
|
||||
return bulbmap
|
||||
}
|
||||
|
||||
def bulbListData(evt) {
|
||||
state.bulbs = evt.jsonData
|
||||
}
|
||||
|
||||
Map getHueBulbs() {
|
||||
state.bulbs = state.bulbs ?: [:]
|
||||
}
|
||||
@@ -295,6 +299,7 @@ def initialize() {
|
||||
state.bridgeRefreshCount = 0
|
||||
state.bulbRefreshCount = 0
|
||||
state.updating = false
|
||||
setupDeviceWatch()
|
||||
if (selectedHue) {
|
||||
addBridge()
|
||||
addBulbs()
|
||||
@@ -317,28 +322,13 @@ def uninstalled(){
|
||||
state.username = null
|
||||
}
|
||||
|
||||
// Handles events to add new bulbs
|
||||
def bulbListHandler(hub, data = "") {
|
||||
def msg = "Bulbs list not processed. Only while in settings menu."
|
||||
def bulbs = [:]
|
||||
if (state.inBulbDiscovery) {
|
||||
def logg = ""
|
||||
log.trace "Adding bulbs to state..."
|
||||
state.bridgeProcessedLightList = true
|
||||
def object = new groovy.json.JsonSlurper().parseText(data)
|
||||
object.each { k,v ->
|
||||
if (v instanceof Map)
|
||||
bulbs[k] = [id: k, name: v.name, type: v.type, modelid: v.modelid, hub:hub]
|
||||
private setupDeviceWatch() {
|
||||
def hub = location.hubs[0]
|
||||
// Make sure that all child devices are enrolled in device watch
|
||||
getChildDevices().each {
|
||||
it.sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${hub?.hub?.hardwareID}\"}")
|
||||
}
|
||||
}
|
||||
def bridge = null
|
||||
if (selectedHue) {
|
||||
bridge = getChildDevice(selectedHue)
|
||||
}
|
||||
bridge.sendEvent(name: "bulbList", value: hub, data: bulbs, isStateChange: true, displayed: false)
|
||||
msg = "${bulbs.size()} bulbs found. ${bulbs}"
|
||||
return msg
|
||||
}
|
||||
|
||||
private upgradeDeviceType(device, newHueType) {
|
||||
def deviceType = getDeviceType(newHueType)
|
||||
@@ -388,7 +378,6 @@ def addBulbs() {
|
||||
if (d) {
|
||||
log.debug "created ${d.displayName} with id $dni"
|
||||
d.completedSetup = true
|
||||
d.refresh()
|
||||
}
|
||||
} else {
|
||||
log.debug "$dni in not longer paired to the Hue Bridge or ID changed"
|
||||
@@ -445,10 +434,11 @@ def addBridge() {
|
||||
d.completedSetup = true
|
||||
log.debug "created ${d.displayName} with id ${d.deviceNetworkId}"
|
||||
def childDevice = getChildDevice(d.deviceNetworkId)
|
||||
childDevice?.sendEvent(name: "status", value: "Online")
|
||||
childDevice?.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false, isStateChange: true)
|
||||
updateBridgeStatus(childDevice)
|
||||
childDevice.sendEvent(name: "idNumber", value: idNumber)
|
||||
|
||||
|
||||
childDevice?.sendEvent(name: "idNumber", value: idNumber)
|
||||
if (vbridge.value.ip && vbridge.value.port) {
|
||||
if (vbridge.value.ip.contains(".")) {
|
||||
childDevice.sendEvent(name: "networkAddress", value: vbridge.value.ip + ":" + vbridge.value.port)
|
||||
@@ -492,25 +482,26 @@ def ssdpBridgeHandler(evt) {
|
||||
def host = ip + ":" + port
|
||||
log.debug "Device ($parsedEvent.mac) was already found in state with ip = $host."
|
||||
def dstate = bridges."${parsedEvent.ssdpUSN.toString()}"
|
||||
def dni = "${parsedEvent.mac}"
|
||||
def d = getChildDevice(dni)
|
||||
def dniReceived = "${parsedEvent.mac}"
|
||||
def currentDni = dstate.mac
|
||||
def d = getChildDevice(dniReceived)
|
||||
def networkAddress = null
|
||||
if (!d) {
|
||||
childDevices.each {
|
||||
if (it.getDeviceDataByName("mac")) {
|
||||
def newDNI = "${it.getDeviceDataByName("mac")}"
|
||||
d = it
|
||||
if (newDNI != it.deviceNetworkId) {
|
||||
def oldDNI = it.deviceNetworkId
|
||||
log.debug "updating dni for device ${it} with $newDNI - previous DNI = ${it.deviceNetworkId}"
|
||||
it.setDeviceNetworkId("${newDNI}")
|
||||
if (oldDNI == selectedHue) {
|
||||
app.updateSetting("selectedHue", newDNI)
|
||||
// There might be a mismatch between bridge DNI and the actual bridge mac address, correct that
|
||||
log.debug "Bridge with $dniReceived not found"
|
||||
def bridge = childDevices.find { it.deviceNetworkId == currentDni }
|
||||
if (bridge != null) {
|
||||
log.warn "Bridge is set to ${bridge.deviceNetworkId}, updating to $dniReceived"
|
||||
bridge.setDeviceNetworkId("${dniReceived}")
|
||||
dstate.mac = dniReceived
|
||||
// Check to see if selectedHue is a valid bridge, otherwise update it
|
||||
def isSelectedValid = bridges?.find {it.value?.mac == selectedHue}
|
||||
if (isSelectedValid == null) {
|
||||
log.warn "Correcting selectedHue in state"
|
||||
app.updateSetting("selectedHue", dniReceived)
|
||||
}
|
||||
doDeviceSync()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updateBridgeStatus(d)
|
||||
if (d.getDeviceDataByName("networkAddress")) {
|
||||
@@ -527,6 +518,18 @@ def ssdpBridgeHandler(evt) {
|
||||
d.sendEvent(name:"networkAddress", value: host)
|
||||
d.updateDataValue("networkAddress", host)
|
||||
}
|
||||
if (dstate.mac != dniReceived) {
|
||||
log.warn "Correcting bridge mac address in state"
|
||||
dstate.mac = dniReceived
|
||||
}
|
||||
if (selectedHue != dniReceived) {
|
||||
// Check to see if selectedHue is a valid bridge, otherwise update it
|
||||
def isSelectedValid = bridges?.find {it.value?.mac == selectedHue}
|
||||
if (isSelectedValid == null) {
|
||||
log.warn "Correcting selectedHue in state"
|
||||
app.updateSetting("selectedHue", dniReceived)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -559,11 +562,8 @@ void lightsHandler(physicalgraph.device.HubResponse hubResponse) {
|
||||
if (isValidSource(hubResponse.mac)) {
|
||||
def body = hubResponse.json
|
||||
if (!body?.state?.on) { //check if first time poll made it here by mistake
|
||||
def bulbs = getHueBulbs()
|
||||
log.debug "Adding bulbs to state!"
|
||||
body.each { k, v ->
|
||||
bulbs[k] = [id: k, name: v.name, type: v.type, modelid: v.modelid, hub: hubResponse.hubId]
|
||||
}
|
||||
updateBulbState(body, hubResponse.hubId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -649,8 +649,7 @@ def locationHandler(evt) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (parsedEvent.headers && parsedEvent.body) {
|
||||
} else if (parsedEvent.headers && parsedEvent.body) {
|
||||
log.trace "HUE BRIDGE RESPONSES"
|
||||
def headerString = parsedEvent.headers.toString()
|
||||
if (headerString?.contains("xml")) {
|
||||
@@ -680,11 +679,8 @@ def locationHandler(evt) {
|
||||
} else {
|
||||
//GET /api/${state.username}/lights response (application/json)
|
||||
if (!body?.state?.on) { //check if first time poll made it here by mistake
|
||||
def bulbs = getHueBulbs()
|
||||
log.debug "Adding bulbs to state!"
|
||||
body.each { k,v ->
|
||||
bulbs[k] = [id: k, name: v.name, type: v.type, modelid: v.modelid, hub:parsedEvent.hub]
|
||||
}
|
||||
updateBulbState(body, parsedEvent.hub)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -719,21 +715,25 @@ def doDeviceSync(){
|
||||
private void updateBridgeStatus(childDevice) {
|
||||
// Update activity timestamp if child device is a valid bridge
|
||||
def vbridges = getVerifiedHueBridges()
|
||||
def vbridge = vbridges.find {"${it.value.mac}".toUpperCase() == childDevice?.device?.deviceNetworkId?.toUpperCase()}
|
||||
def vbridge = vbridges.find {
|
||||
"${it.value.mac}".toUpperCase() == childDevice?.device?.deviceNetworkId?.toUpperCase()
|
||||
}
|
||||
vbridge?.value?.lastActivity = now()
|
||||
if(vbridge) {
|
||||
if (vbridge && childDevice?.device?.currentValue("status") == "Offline") {
|
||||
log.debug "$childDevice is back Online"
|
||||
childDevice?.sendEvent(name: "status", value: "Online")
|
||||
childDevice?.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false, isStateChange: true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all Hue bridges have been heard from in the last 16 minutes, if not an Offline event will be sent
|
||||
* for the bridge. Also, set ID number on bridge if not done previously.
|
||||
* Check if all Hue bridges have been heard from in the last 11 minutes, if not an Offline event will be sent
|
||||
* for the bridge and all connected lights. Also, set ID number on bridge if not done previously.
|
||||
*/
|
||||
private void checkBridgeStatus() {
|
||||
def bridges = getHueBridges()
|
||||
// Check if each bridge has been heard from within the last 16 minutes (3 poll intervals times 5 minutes plus buffer)
|
||||
def time = now() - (1000 * 60 * 16)
|
||||
// Check if each bridge has been heard from within the last 11 minutes (2 poll intervals times 5 minutes plus buffer)
|
||||
def time = now() - (1000 * 60 * 11)
|
||||
bridges.each {
|
||||
def d = getChildDevice(it.value.mac)
|
||||
if (d) {
|
||||
@@ -744,9 +744,24 @@ private void checkBridgeStatus() {
|
||||
}
|
||||
|
||||
if (it.value.lastActivity < time) { // it.value.lastActivity != null &&
|
||||
log.warn "Bridge $it.key is Offline"
|
||||
if (d.currentStatus == "Online") {
|
||||
log.warn "$d is Offline"
|
||||
d.sendEvent(name: "status", value: "Offline")
|
||||
} else {
|
||||
d.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false, isStateChange: true)
|
||||
|
||||
Calendar currentTime = Calendar.getInstance()
|
||||
getChildDevices().each {
|
||||
def id = getId(it)
|
||||
if (state.bulbs[id]?.online == true) {
|
||||
state.bulbs[id]?.online = false
|
||||
state.bulbs[id]?.unreachableSince = currentTime.getTimeInMillis()
|
||||
it.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false, isStateChange: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (d.currentStatus == "Offline") {
|
||||
log.debug "$d is back Online"
|
||||
d.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false, isStateChange: true)
|
||||
d.sendEvent(name: "status", value: "Online")//setOnline(false)
|
||||
}
|
||||
}
|
||||
@@ -762,6 +777,31 @@ def isInBulbDiscovery() {
|
||||
return state.inBulbDiscovery
|
||||
}
|
||||
|
||||
private updateBulbState(messageBody, hub) {
|
||||
def bulbs = getHueBulbs()
|
||||
|
||||
// Copy of bulbs used to locate old lights in state that are no longer on bridge
|
||||
def toRemove = [:]
|
||||
toRemove << bulbs
|
||||
|
||||
messageBody.each { k,v ->
|
||||
|
||||
if (v instanceof Map) {
|
||||
if (bulbs[k] == null) {
|
||||
bulbs[k] = [:]
|
||||
}
|
||||
bulbs[k] << [id: k, name: v.name, type: v.type, modelid: v.modelid, hub:hub, remove: false]
|
||||
toRemove.remove(k)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove bulbs from state that are no longer discovered
|
||||
toRemove.each { k,v ->
|
||||
log.warn "${bulbs[k].name} no longer exists on bridge, removing"
|
||||
bulbs.remove(k)
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////
|
||||
//CHILD DEVICE METHODS
|
||||
/////////////////////////////////////
|
||||
@@ -785,8 +825,7 @@ def parse(childDevice, description) {
|
||||
if (body instanceof java.util.Map) {
|
||||
// get (poll) reponse
|
||||
return handlePoll(body)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
//put response
|
||||
return handleCommandResponse(body)
|
||||
}
|
||||
@@ -798,10 +837,12 @@ def parse(childDevice, description) {
|
||||
}
|
||||
|
||||
// Philips Hue priority for color is xy > ct > hs
|
||||
// For SmartThings, try to always send hue, sat and hex
|
||||
private sendColorEvents(device, xy, hue, sat, ct, colormode = null) {
|
||||
if (device == null || (xy == null && hue == null && sat == null && ct == null))
|
||||
return
|
||||
|
||||
def events = [:]
|
||||
// For now, only care about changing color temperature if requested by user
|
||||
if (ct != null && (colormode == "ct" || (xy == null && hue == null && sat == null))) {
|
||||
// for some reason setting Hue to their specified minimum off 153 yields 154, dealt with below
|
||||
@@ -815,13 +856,13 @@ private sendColorEvents(device, xy, hue, sat, ct, colormode = null) {
|
||||
if (hue != null) {
|
||||
// 0-65535
|
||||
def value = Math.min(Math.round(hue * 100 / 65535), 65535) as int
|
||||
device.sendEvent([name: "hue", value: value, descriptionText: "Color has changed"])
|
||||
events["hue"] = [name: "hue", value: value, descriptionText: "Color has changed", displayed: false]
|
||||
}
|
||||
|
||||
if (sat != null) {
|
||||
// 0-254
|
||||
def value = Math.round(sat * 100 / 254) as int
|
||||
device.sendEvent([name: "saturation", value: value, descriptionText: "Color has changed"])
|
||||
events["saturation"] = [name: "saturation", value: value, descriptionText: "Color has changed", displayed: false]
|
||||
}
|
||||
|
||||
// Following is used to decide what to base hex calculations on since it is preferred to return a colorchange in hex
|
||||
@@ -833,17 +874,28 @@ private sendColorEvents(device, xy, hue, sat, ct, colormode = null) {
|
||||
def model = state.bulbs[id]?.modelid
|
||||
def hex = colorFromXY(xy, model)
|
||||
|
||||
// TODO Disabled until a solution for the jumping color picker can be figured out
|
||||
//device.sendEvent([name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed", displayed: false])
|
||||
// Create Hue and Saturation events if not previously existing
|
||||
def hsv = hexToHsv(hex)
|
||||
if (events["hue"] == null)
|
||||
events["hue"] = [name: "hue", value: hsv[0], descriptionText: "Color has changed", displayed: false]
|
||||
if (events["saturation"] == null)
|
||||
events["saturation"] = [name: "saturation", value: hsv[1], descriptionText: "Color has changed", displayed: false]
|
||||
|
||||
events["color"] = [name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed", displayed: true]
|
||||
} else if (colormode == "hs" || colormode == null) {
|
||||
// colormode is "hs" or "xy" is missing, default to follow hue/sat which is already handled above
|
||||
def hueValue = (hue != null) ? events["hue"].value : Integer.parseInt("$device.currentHue")
|
||||
def satValue = (sat != null) ? events["saturation"].value : Integer.parseInt("$device.currentSaturation")
|
||||
|
||||
// TODO Disabled until the standard behavior of lights is defined (hue and sat events are sent above)
|
||||
//def hex = colorUtil.hslToHex((int) device.currentHue, (int) device.currentSaturation)
|
||||
// device.sendEvent([name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed"])
|
||||
|
||||
def hex = hsvToHex(hueValue, satValue)
|
||||
events["color"] = [name: "color", value: hex.toUpperCase(), descriptionText: "Color has changed", displayed: true]
|
||||
}
|
||||
|
||||
return debug
|
||||
boolean sendColorChanged = false
|
||||
events.each {
|
||||
device.sendEvent(it.value)
|
||||
}
|
||||
}
|
||||
|
||||
private sendBasicEvents(device, param, value) {
|
||||
@@ -884,8 +936,6 @@ private handleCommandResponse(body) {
|
||||
def updates = [:]
|
||||
|
||||
body.each { payload ->
|
||||
log.debug $payload
|
||||
|
||||
if (payload?.success) {
|
||||
def childDeviceNetworkId = app.id + "/"
|
||||
def eventType
|
||||
@@ -899,14 +949,22 @@ private handleCommandResponse(body) {
|
||||
updates[childDeviceNetworkId]."$eventType" = v
|
||||
}
|
||||
}
|
||||
} else if (payload.error) {
|
||||
log.warn "Error returned from Hue bridge error = ${body?.error}"
|
||||
} else if (payload?.error) {
|
||||
log.warn "Error returned from Hue bridge, error = ${payload?.error}"
|
||||
// Check for unauthorized user
|
||||
if (payload?.error?.type?.value == 1) {
|
||||
log.error "Hue username is not valid"
|
||||
state.refreshUsernameNeeded = true
|
||||
state.username = null
|
||||
}
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// send events for each update found above (order of events should be same as handlePoll())
|
||||
updates.each { childDeviceNetworkId, params ->
|
||||
def device = getChildDevice(childDeviceNetworkId)
|
||||
def id = getId(device)
|
||||
sendBasicEvents(device, "on", params.on)
|
||||
sendBasicEvents(device, "bri", params.bri)
|
||||
sendColorEvents(device, params.xy, params.hue, params.sat, params.ct)
|
||||
@@ -932,21 +990,47 @@ private handleCommandResponse(body) {
|
||||
* @return empty array
|
||||
*/
|
||||
private handlePoll(body) {
|
||||
if (state.updating) {
|
||||
// If user just executed commands, then ignore poll to not confuse the turning on/off state
|
||||
return []
|
||||
}
|
||||
// Used to track "unreachable" time
|
||||
// Device is considered "offline" if it has been in the "unreachable" state for
|
||||
// 11 minutes (e.g. two poll intervals)
|
||||
// Note, Hue Bridge marks devices as "unreachable" often even when they accept commands
|
||||
Calendar time11 = Calendar.getInstance()
|
||||
time11.add(Calendar.MINUTE, -11)
|
||||
Calendar currentTime = Calendar.getInstance()
|
||||
|
||||
def bulbs = getChildDevices()
|
||||
for (bulb in body) {
|
||||
def device = bulbs.find { it.deviceNetworkId == "${app.id}/${bulb.key}" }
|
||||
if (device) {
|
||||
if (bulb.value.state?.reachable) {
|
||||
if (state.bulbs[bulb.key]?.online == false || state.bulbs[bulb.key]?.online == null) {
|
||||
// light just came back online, notify device watch
|
||||
device.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false, isStateChange: true)
|
||||
log.debug "$device is Online"
|
||||
}
|
||||
// Mark light as "online"
|
||||
state.bulbs[bulb.key]?.unreachableSince = null
|
||||
state.bulbs[bulb.key]?.online = true
|
||||
} else {
|
||||
if (state.bulbs[bulb.key]?.unreachableSince == null) {
|
||||
// Store the first time where device was reported as "unreachable"
|
||||
state.bulbs[bulb.key]?.unreachableSince = currentTime.getTimeInMillis()
|
||||
}
|
||||
if (state.bulbs[bulb.key]?.online || state.bulbs[bulb.key]?.online == null) {
|
||||
// Check if device was "unreachable" for more than 11 minutes and mark "offline" if necessary
|
||||
if (state.bulbs[bulb.key]?.unreachableSince < time11.getTimeInMillis() || state.bulbs[bulb.key]?.online == null) {
|
||||
log.warn "$device went Offline"
|
||||
state.bulbs[bulb.key]?.online = false
|
||||
device.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false, isStateChange: true)
|
||||
}
|
||||
}
|
||||
log.warn "$device may not reachable by Hue bridge"
|
||||
}
|
||||
// If user just executed commands, then do not send events to avoid confusing the turning on/off state
|
||||
if (!state.updating) {
|
||||
sendBasicEvents(device, "on", bulb.value?.state?.on)
|
||||
sendBasicEvents(device, "bri", bulb.value?.state?.bri)
|
||||
sendColorEvents(device, bulb.value?.state?.xy, bulb.value?.state?.hue, bulb.value?.state?.sat, bulb.value?.state?.ct, bulb.value?.state?.colormode)
|
||||
} else {
|
||||
log.warn "$device is not reachable by Hue bridge"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -980,22 +1064,25 @@ def hubVerification(bodytext) {
|
||||
|
||||
def on(childDevice) {
|
||||
log.debug "Executing 'on'"
|
||||
def id = getId(childDevice)
|
||||
updateInProgress()
|
||||
createSwitchEvent(childDevice, "on")
|
||||
put("lights/${getId(childDevice)}/state", [on: true])
|
||||
put("lights/$id/state", [on: true])
|
||||
return "Bulb is turning On"
|
||||
}
|
||||
|
||||
def off(childDevice) {
|
||||
log.debug "Executing 'off'"
|
||||
def id = getId(childDevice)
|
||||
updateInProgress()
|
||||
createSwitchEvent(childDevice, "off")
|
||||
put("lights/${getId(childDevice)}/state", [on: false])
|
||||
put("lights/$id/state", [on: false])
|
||||
return "Bulb is turning Off"
|
||||
}
|
||||
|
||||
def setLevel(childDevice, percent) {
|
||||
log.debug "Executing 'setLevel'"
|
||||
def id = getId(childDevice)
|
||||
updateInProgress()
|
||||
// 1 - 254
|
||||
def level
|
||||
@@ -1010,48 +1097,51 @@ def setLevel(childDevice, percent) {
|
||||
// that means that the light will still be on when on is called next time
|
||||
// Lets emulate that here
|
||||
if (percent > 0) {
|
||||
put("lights/${getId(childDevice)}/state", [bri: level, on: true])
|
||||
put("lights/$id/state", [bri: level, on: true])
|
||||
} else {
|
||||
put("lights/${getId(childDevice)}/state", [on: false])
|
||||
put("lights/$id/state", [on: false])
|
||||
}
|
||||
return "Setting level to $percent"
|
||||
}
|
||||
|
||||
def setSaturation(childDevice, percent) {
|
||||
log.debug "Executing 'setSaturation($percent)'"
|
||||
def id = getId(childDevice)
|
||||
updateInProgress()
|
||||
// 0 - 254
|
||||
def level = Math.min(Math.round(percent * 254 / 100), 254)
|
||||
// TODO should this be done by app only or should we default to on?
|
||||
createSwitchEvent(childDevice, "on")
|
||||
put("lights/${getId(childDevice)}/state", [sat: level, on: true])
|
||||
put("lights/$id/state", [sat: level, on: true])
|
||||
return "Setting saturation to $percent"
|
||||
}
|
||||
|
||||
def setHue(childDevice, percent) {
|
||||
log.debug "Executing 'setHue($percent)'"
|
||||
def id = getId(childDevice)
|
||||
updateInProgress()
|
||||
// 0 - 65535
|
||||
def level = Math.min(Math.round(percent * 65535 / 100), 65535)
|
||||
// TODO should this be done by app only or should we default to on?
|
||||
createSwitchEvent(childDevice, "on")
|
||||
put("lights/${getId(childDevice)}/state", [hue: level, on: true])
|
||||
put("lights/$id/state", [hue: level, on: true])
|
||||
return "Setting hue to $percent"
|
||||
}
|
||||
|
||||
def setColorTemperature(childDevice, huesettings) {
|
||||
log.debug "Executing 'setColorTemperature($huesettings)'"
|
||||
def id = getId(childDevice)
|
||||
updateInProgress()
|
||||
// 153 (6500K) to 500 (2000K)
|
||||
def ct = hueSettings == 6500 ? 153 : Math.round(1000000/huesettings)
|
||||
createSwitchEvent(childDevice, "on")
|
||||
put("lights/${getId(childDevice)}/state", [ct: ct, on: true])
|
||||
put("lights/$id/state", [ct: ct, on: true])
|
||||
return "Setting color temperature to $percent"
|
||||
}
|
||||
|
||||
def setColor(childDevice, huesettings) {
|
||||
log.debug "Executing 'setColor($huesettings)'"
|
||||
|
||||
def id = getId(childDevice)
|
||||
updateInProgress()
|
||||
|
||||
def value = [:]
|
||||
@@ -1059,25 +1149,21 @@ def setColor(childDevice, huesettings) {
|
||||
def sat = null
|
||||
def xy = null
|
||||
|
||||
// For now ignore model to get a consistent color if same color is set across multiple devices
|
||||
// def model = state.bulbs[getId(childDevice)]?.modelid
|
||||
if (huesettings.hex != null) {
|
||||
// value.xy = calculateXY(huesettings.hex, model)
|
||||
// Once groups, or scenes are introduced it might be a good idea to use unique models again
|
||||
value.xy = calculateXY(huesettings.hex)
|
||||
}
|
||||
|
||||
// Prefer hue/sat over hex to make sure it works with the majority of the smartapps
|
||||
if (huesettings.hue != null || huesettings.sat != null) {
|
||||
// If both hex and hue/sat are set, send all values to bridge to get hue/sat in response from bridge to
|
||||
// generate hue/sat events even though bridge will prioritize XY when setting color
|
||||
if (huesettings.hue != null)
|
||||
value.hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
|
||||
else
|
||||
value.hue = Math.min(Math.round(childDevice.device?.currentValue("hue") * 65535 / 100), 65535)
|
||||
|
||||
if (huesettings.saturation != null)
|
||||
value.sat = Math.min(Math.round(huesettings.saturation * 254 / 100), 254)
|
||||
else
|
||||
value.sat = Math.min(Math.round(childDevice.device?.currentValue("saturation") * 254 / 100), 254)
|
||||
} else if (huesettings.hex != null) {
|
||||
// For now ignore model to get a consistent color if same color is set across multiple devices
|
||||
// def model = state.bulbs[getId(childDevice)]?.modelid
|
||||
// value.xy = calculateXY(huesettings.hex, model)
|
||||
// Once groups, or scenes are introduced it might be a good idea to use unique models again
|
||||
value.xy = calculateXY(huesettings.hex)
|
||||
}
|
||||
|
||||
/* Disabled for now due to bad behavior via Lightning Wizard
|
||||
if (!value.xy) {
|
||||
@@ -1108,15 +1194,14 @@ def setColor(childDevice, huesettings) {
|
||||
value.on = false
|
||||
|
||||
createSwitchEvent(childDevice, value.on ? "on" : "off")
|
||||
put("lights/${getId(childDevice)}/state", value)
|
||||
put("lights/$id/state", value)
|
||||
return "Setting color to $value"
|
||||
}
|
||||
|
||||
private getId(childDevice) {
|
||||
if (childDevice.device?.deviceNetworkId?.startsWith("HUE")) {
|
||||
return childDevice.device?.deviceNetworkId[3..-1]
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return childDevice.device?.deviceNetworkId.split("/")[-1]
|
||||
}
|
||||
}
|
||||
@@ -1125,10 +1210,12 @@ 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
|
||||
HOST: ${host}
|
||||
sendHubCommand(new physicalgraph.device.HubAction("GET ${uri} HTTP/1.1\r\n" +
|
||||
"HOST: ${host}\r\n\r\n", physicalgraph.device.Protocol.LAN, selectedHue))
|
||||
}
|
||||
|
||||
""", physicalgraph.device.Protocol.LAN, selectedHue))
|
||||
private isOnline(id) {
|
||||
return (state.bulbs[id]?.online != null && state.bulbs[id]?.online) || state.bulbs[id]?.online == null
|
||||
}
|
||||
|
||||
private put(path, body) {
|
||||
@@ -1140,13 +1227,11 @@ private put(path, body) {
|
||||
log.debug "PUT: $host$uri"
|
||||
log.debug "BODY: ${bodyJSON}"
|
||||
|
||||
sendHubCommand(new physicalgraph.device.HubAction("""PUT $uri HTTP/1.1
|
||||
HOST: ${host}
|
||||
Content-Length: ${length}
|
||||
|
||||
${bodyJSON}
|
||||
""", physicalgraph.device.Protocol.LAN, "${selectedHue}"))
|
||||
|
||||
sendHubCommand(new physicalgraph.device.HubAction("PUT $uri HTTP/1.1\r\n" +
|
||||
"HOST: ${host}\r\n" +
|
||||
"Content-Length: ${length}\r\n" +
|
||||
"\r\n" +
|
||||
"${bodyJSON}", physicalgraph.device.Protocol.LAN, "${selectedHue}"))
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1197,8 +1282,8 @@ def convertBulbListToMap() {
|
||||
try {
|
||||
if (state.bulbs instanceof java.util.List) {
|
||||
def map = [:]
|
||||
state.bulbs.unique {it.id}.each { bulb ->
|
||||
map << ["${bulb.id}":["id":bulb.id, "name":bulb.name, "type": bulb.type, "modelid": bulb.modelid, "hub":bulb.hub]]
|
||||
state.bulbs?.unique {it.id}.each { bulb ->
|
||||
map << ["${bulb.id}":["id":bulb.id, "name":bulb.name, "type": bulb.type, "modelid": bulb.modelid, "hub":bulb.hub, "online": bulb.online]]
|
||||
}
|
||||
state.bulbs = map
|
||||
}
|
||||
@@ -1604,3 +1689,102 @@ private boolean checkPointInLampsReach(p, colorPoints) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an RGB color in hex to HSV/HSB.
|
||||
* Algorithm based on http://en.wikipedia.org/wiki/HSV_color_space.
|
||||
*
|
||||
* @param colorStr color value in hex (#ff03d3)
|
||||
*
|
||||
* @return HSV representation in an array (0-100) [hue, sat, value]
|
||||
*/
|
||||
def hexToHsv(colorStr){
|
||||
def r = Integer.valueOf( colorStr.substring( 1, 3 ), 16 ) / 255
|
||||
def g = Integer.valueOf( colorStr.substring( 3, 5 ), 16 ) / 255
|
||||
def b = Integer.valueOf( colorStr.substring( 5, 7 ), 16 ) / 255
|
||||
|
||||
def max = Math.max(Math.max(r, g), b)
|
||||
def min = Math.min(Math.min(r, g), b)
|
||||
|
||||
def h, s, v = max
|
||||
|
||||
def d = max - min
|
||||
s = max == 0 ? 0 : d / max
|
||||
|
||||
if(max == min){
|
||||
h = 0
|
||||
}else{
|
||||
switch(max){
|
||||
case r: h = (g - b) / d + (g < b ? 6 : 0); break
|
||||
case g: h = (b - r) / d + 2; break
|
||||
case b: h = (r - g) / d + 4; break
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return [Math.round(h * 100), Math.round(s * 100), Math.round(v * 100)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts HSV/HSB color to RGB in hex.
|
||||
* Algorithm based on http://en.wikipedia.org/wiki/HSV_color_space.
|
||||
*
|
||||
* @param hue hue 0-100
|
||||
* @param sat saturation 0-100
|
||||
* @param value value 0-100 (defaults to 100)
|
||||
|
||||
* @return the color in hex (#ff03d3)
|
||||
*/
|
||||
def hsvToHex(hue, sat, value = 100){
|
||||
def r, g, b;
|
||||
def h = hue / 100
|
||||
def s = sat / 100
|
||||
def v = value / 100
|
||||
|
||||
def i = Math.floor(h * 6)
|
||||
def f = h * 6 - i
|
||||
def p = v * (1 - s)
|
||||
def q = v * (1 - f * s)
|
||||
def t = v * (1 - (1 - f) * s)
|
||||
|
||||
switch (i % 6) {
|
||||
case 0:
|
||||
r = v
|
||||
g = t
|
||||
b = p
|
||||
break
|
||||
case 1:
|
||||
r = q
|
||||
g = v
|
||||
b = p
|
||||
break
|
||||
case 2:
|
||||
r = p
|
||||
g = v
|
||||
b = t
|
||||
break
|
||||
case 3:
|
||||
r = p
|
||||
g = q
|
||||
b = v
|
||||
break
|
||||
case 4:
|
||||
r = t
|
||||
g = p
|
||||
b = v
|
||||
break
|
||||
case 5:
|
||||
r = v
|
||||
g = p
|
||||
b = q
|
||||
break
|
||||
}
|
||||
|
||||
// Converting float components to int components.
|
||||
def r1 = String.format("%02X", (int) (r * 255.0f))
|
||||
def g1 = String.format("%02X", (int) (g * 255.0f))
|
||||
def b1 = String.format("%02X", (int) (b * 255.0f))
|
||||
|
||||
return "#$r1$g1$b1"
|
||||
}
|
||||
|
||||
|
||||
@@ -53,14 +53,14 @@ def accelerationActiveHandler(evt) {
|
||||
def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1
|
||||
|
||||
if (alreadySentSms) {
|
||||
log.debug "SMS already sent to $phone1 within the last $deltaSeconds seconds"
|
||||
log.debug "SMS already sent within the last $deltaSeconds seconds"
|
||||
} else {
|
||||
if (location.contactBookEnabled) {
|
||||
log.debug "$accelerationSensor has moved, texting contacts: ${recipients?.size()}"
|
||||
log.debug "accelerationSensor has moved, texting contacts: ${recipients?.size()}"
|
||||
sendNotificationToContacts("${accelerationSensor.label ?: accelerationSensor.name} moved", recipients)
|
||||
}
|
||||
else {
|
||||
log.debug "$accelerationSensor has moved, texting $phone1"
|
||||
log.debug "accelerationSensor has moved, sending text message"
|
||||
sendSms(phone1, "${accelerationSensor.label ?: accelerationSensor.name} moved")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,10 +69,10 @@ def temperatureHandler(evt) {
|
||||
def alreadySentSms = recentEvents.count { it.doubleValue >= tooHot } > 1
|
||||
|
||||
if (alreadySentSms) {
|
||||
log.debug "SMS already sent to $phone1 within the last $deltaMinutes minutes"
|
||||
log.debug "SMS already sent within the last $deltaMinutes minutes"
|
||||
// TODO: Send "Temperature back to normal" SMS, turn switch off
|
||||
} else {
|
||||
log.debug "Temperature rose above $tooHot: sending SMS to $phone1 and activating $mySwitch"
|
||||
log.debug "Temperature rose above $tooHot: sending SMS and activating $mySwitch"
|
||||
def tempScale = location.temperatureScale ?: "F"
|
||||
send("${temperatureSensor1.displayName} is too hot, reporting a temperature of ${evt.value}${evt.unit?:tempScale}")
|
||||
switch1?.on()
|
||||
|
||||
@@ -51,8 +51,8 @@ def authPage() {
|
||||
def description = "Tap to enter LIFX credentials"
|
||||
def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" // this triggers oauthInit() below
|
||||
// def redirectUrl = "${apiServerUrl}"
|
||||
log.debug "app id: ${app.id}"
|
||||
log.debug "redirect url: ${redirectUrl}"
|
||||
// log.debug "app id: ${app.id}"
|
||||
// log.debug "redirect url: ${redirectUrl}"s
|
||||
return dynamicPage(name: "Credentials", title: "Connect to LIFX", nextPage: null, uninstall: true, install:true) {
|
||||
section {
|
||||
href(url:redirectUrl, required:true, title:"Connect to LIFX", description:"Tap here to connect your LIFX account")
|
||||
@@ -242,8 +242,6 @@ def installed() {
|
||||
} else {
|
||||
initialize()
|
||||
}
|
||||
// Check for new devices and remove old ones every 3 hours
|
||||
runEvery3Hours('updateDevices')
|
||||
}
|
||||
|
||||
// called after settings are changed
|
||||
@@ -271,9 +269,19 @@ private removeChildDevices(devices) {
|
||||
def initialize() {
|
||||
log.debug "initialize"
|
||||
updateDevices()
|
||||
// Check for new devices and remove old ones every 3 hours
|
||||
runEvery5Minutes('updateDevices')
|
||||
setupDeviceWatch()
|
||||
}
|
||||
|
||||
// Misc
|
||||
private setupDeviceWatch() {
|
||||
def hub = location.hubs[0]
|
||||
// Make sure that all child devices are enrolled in device watch
|
||||
getChildDevices().each {
|
||||
it.sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${hub?.hub?.hardwareID}\"}")
|
||||
}
|
||||
}
|
||||
|
||||
Map apiRequestHeaders() {
|
||||
return ["Authorization": "Bearer ${state.lifxAccessToken}",
|
||||
@@ -372,33 +380,47 @@ def updateDevices() {
|
||||
def childDevice = getChildDevice(device.id)
|
||||
selectors.add("${device.id}")
|
||||
if (!childDevice) {
|
||||
log.info("Adding device ${device.id}: ${device.product}")
|
||||
def data = [
|
||||
label: device.label,
|
||||
level: Math.round((device.brightness ?: 1) * 100),
|
||||
switch: device.connected ? device.power : "unreachable",
|
||||
colorTemperature: device.color.kelvin
|
||||
]
|
||||
// log.info("Adding device ${device.id}: ${device.product}")
|
||||
if (device.product.capabilities.has_color) {
|
||||
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(app.namespace, "LIFX Color Bulb", device.id, null, data)
|
||||
childDevice = addChildDevice(app.namespace, "LIFX Color Bulb", device.id, null, ["label": device.label, "completedSetup": true])
|
||||
} else {
|
||||
childDevice = addChildDevice(app.namespace, "LIFX White Bulb", device.id, null, data)
|
||||
childDevice = addChildDevice(app.namespace, "LIFX White Bulb", device.id, null, ["label": device.label, "completedSetup": true])
|
||||
}
|
||||
}
|
||||
|
||||
if (device.product.capabilities.has_color) {
|
||||
childDevice.sendEvent(name: "color", value: colorUtil.hslToHex((device.color.hue / 3.6) as int, (device.color.saturation * 100) as int))
|
||||
childDevice.sendEvent(name: "hue", value: device.color.hue / 3.6)
|
||||
childDevice.sendEvent(name: "saturation", value: device.color.saturation * 100)
|
||||
}
|
||||
childDevice.sendEvent(name: "label", value: device.label)
|
||||
childDevice.sendEvent(name: "level", value: Math.round((device.brightness ?: 1) * 100))
|
||||
childDevice.sendEvent(name: "switch.setLevel", value: Math.round((device.brightness ?: 1) * 100))
|
||||
childDevice.sendEvent(name: "switch", value: device.power)
|
||||
childDevice.sendEvent(name: "colorTemperature", value: device.color.kelvin)
|
||||
childDevice.sendEvent(name: "model", value: device.product.name)
|
||||
|
||||
if (state.devices[device.id] == null) {
|
||||
// State missing, add it and set it to opposite status as current status to provoke event below
|
||||
state.devices[device.id] = [online: !device.connected]
|
||||
}
|
||||
|
||||
if (!state.devices[device.id]?.online && device.connected) {
|
||||
// Device came online after being offline
|
||||
childDevice?.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false)
|
||||
log.debug "$device is back Online"
|
||||
} else if (state.devices[device.id]?.online && !device.connected) {
|
||||
// Device went offline after being online
|
||||
childDevice?.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false)
|
||||
log.debug "$device went Offline"
|
||||
}
|
||||
state.devices[device.id] = [online: device.connected]
|
||||
}
|
||||
getChildDevices().findAll { !selectors.contains("${it.deviceNetworkId}") }.each {
|
||||
log.info("Deleting ${it.deviceNetworkId}")
|
||||
state.devices[it.deviceNetworkId] = null
|
||||
deleteChildDevice(it.deviceNetworkId)
|
||||
}
|
||||
runIn(1, 'refreshDevices') // Asynchronously refresh devices so we don't block
|
||||
}
|
||||
|
||||
def refreshDevices() {
|
||||
log.info("Refreshing all devices...")
|
||||
getChildDevices().each { device ->
|
||||
device.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user