mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-09 13:21:53 +00:00
Compare commits
297 Commits
MSA-63-1
...
DVCSMP-144
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eae2a9ca08 | ||
|
|
0c87601c64 | ||
|
|
a6bec43f2a | ||
|
|
5ec82217ac | ||
|
|
3b89368d45 | ||
|
|
12f6039de5 | ||
|
|
65c9da32e7 | ||
|
|
7147770e2d | ||
|
|
5eb33eca19 | ||
|
|
ec321ce85d | ||
|
|
7669bec0bc | ||
|
|
a20a58bd48 | ||
|
|
efabd07dea | ||
|
|
1e55f62048 | ||
|
|
12b18eae08 | ||
|
|
aad82e10ef | ||
|
|
b11b362c60 | ||
|
|
e1853b8e50 | ||
|
|
968834e33e | ||
|
|
e51a38eb28 | ||
|
|
553b45a3f3 | ||
|
|
d9ab3bca00 | ||
|
|
56cfe9e936 | ||
|
|
3a7abd6169 | ||
|
|
d69abb64bd | ||
|
|
7429ecc83b | ||
|
|
112a35f5db | ||
|
|
0d214b742e | ||
|
|
c297564665 | ||
|
|
26ab32565b | ||
|
|
c20026b376 | ||
|
|
9733947fea | ||
|
|
6abf8c7f20 | ||
|
|
fe505ddc9f | ||
|
|
f4034f5ccf | ||
|
|
c1c2431299 | ||
|
|
39f0c49ea6 | ||
|
|
ed5a409c63 | ||
|
|
8453292038 | ||
|
|
e98a04a1b4 | ||
|
|
41e95b9248 | ||
|
|
63f20c912d | ||
|
|
837d2d0cfd | ||
|
|
629c4cc231 | ||
|
|
f12684565c | ||
|
|
51e727b91a | ||
|
|
49a858eb5c | ||
|
|
112a4087b0 | ||
|
|
132d8fc9d8 | ||
|
|
7def620f04 | ||
|
|
5b0b239caa | ||
|
|
3ea70fecad | ||
|
|
358cf261e8 | ||
|
|
f420907043 | ||
|
|
bf915b49dc | ||
|
|
1c2a65e313 | ||
|
|
075fdf0974 | ||
|
|
21041570db | ||
|
|
8c3daf5f34 | ||
|
|
f3138501e8 | ||
|
|
2dfb43f841 | ||
|
|
f9b5e2ba79 | ||
|
|
1f86001418 | ||
|
|
e4b010eb46 | ||
|
|
c921121645 | ||
|
|
9be808a3eb | ||
|
|
6778f8d58a | ||
|
|
961ee321ac | ||
|
|
22fb4e36c6 | ||
|
|
18bfa87948 | ||
|
|
7fc4aaa341 | ||
|
|
d1504e9a3c | ||
|
|
91fefe536d | ||
|
|
fe4a2ed3d0 | ||
|
|
e529624d36 | ||
|
|
6ba37caa03 | ||
|
|
8d0fa7f561 | ||
|
|
6ede225715 | ||
|
|
30844676b6 | ||
|
|
94aa114ccb | ||
|
|
a9e68d086c | ||
|
|
d427ab8709 | ||
|
|
a205a94d78 | ||
|
|
e775752496 | ||
|
|
9318e9a311 | ||
|
|
e1c52454c6 | ||
|
|
145fce2062 | ||
|
|
e9996b9fd7 | ||
|
|
8be585e544 | ||
|
|
e114fafd56 | ||
|
|
e6367a7832 | ||
|
|
c5da3fe4a0 | ||
|
|
1b9d2fe9ce | ||
|
|
90dee51255 | ||
|
|
7b7fdd43cd | ||
|
|
9059718818 | ||
|
|
9d7c66c7af | ||
|
|
8e81967227 | ||
|
|
290e8e4129 | ||
|
|
8c6c68f102 | ||
|
|
661f8b3bc0 | ||
|
|
96f2c5ed8b | ||
|
|
a62d825f69 | ||
|
|
1890147221 | ||
|
|
2fb5f8c78c | ||
|
|
b44356248c | ||
|
|
c30af84d70 | ||
|
|
5cf72c644c | ||
|
|
8ae9b06022 | ||
|
|
5e93bca030 | ||
|
|
71fc8e7f5f | ||
|
|
30dedde0df | ||
|
|
8b6942525d | ||
|
|
1a8eab065b | ||
|
|
74e3e9f70d | ||
|
|
738d41f5af | ||
|
|
cf69d6127d | ||
|
|
ef4a32ac2e | ||
|
|
ac7592d132 | ||
|
|
a0a3cd7cf9 | ||
|
|
14399f8fc8 | ||
|
|
1f76b9e960 | ||
|
|
5d1630b367 | ||
|
|
0af48a5caf | ||
|
|
a5aeeed77f | ||
|
|
0445b415f7 | ||
|
|
303219fcc5 | ||
|
|
4e88a3ac60 | ||
|
|
9b8be908e2 | ||
|
|
ea65ed58dc | ||
|
|
d19ec0b525 | ||
|
|
9654c27ca8 | ||
|
|
dec9ff20b0 | ||
|
|
c62fd52a26 | ||
|
|
1195161232 | ||
|
|
5f1ff8a5c6 | ||
|
|
7aabd9bc5f | ||
|
|
f260e36d54 | ||
|
|
594b61c852 | ||
|
|
c416560f19 | ||
|
|
1ee768016f | ||
|
|
54e5334cca | ||
|
|
8c55b6314a | ||
|
|
ac5f15efd8 | ||
|
|
bbc680746e | ||
|
|
702f0f87d0 | ||
|
|
d830c1fae0 | ||
|
|
f337e8a085 | ||
|
|
8f6201507a | ||
|
|
0171644a77 | ||
|
|
58187006b7 | ||
|
|
5e6bfb5857 | ||
|
|
2d22b5a384 | ||
|
|
10dee44c0a | ||
|
|
20e5b51aef | ||
|
|
20f1a76889 | ||
|
|
6db15b12c1 | ||
|
|
aa93850c62 | ||
|
|
47210ca8b4 | ||
|
|
cbd5c91d52 | ||
|
|
dcfc16cf12 | ||
|
|
794ff6b68a | ||
|
|
ef21fd4257 | ||
|
|
1965f10584 | ||
|
|
9c9fba0939 | ||
|
|
725f9ebec7 | ||
|
|
e217805d98 | ||
|
|
ff39270ba4 | ||
|
|
d2ece83b47 | ||
|
|
66dbc02274 | ||
|
|
5f899a48d0 | ||
|
|
72899ee036 | ||
|
|
9ff67e9e17 | ||
|
|
f6791d1744 | ||
|
|
8f31b48974 | ||
|
|
b6dd5168d1 | ||
|
|
8a37d5715a | ||
|
|
4ef1d12c61 | ||
|
|
045a024bca | ||
|
|
4d5bf094aa | ||
|
|
9d016839c8 | ||
|
|
e4ce916d8f | ||
|
|
d6668a1e86 | ||
|
|
ecb975540b | ||
|
|
cd81871f90 | ||
|
|
73415f59e1 | ||
|
|
30fdb92141 | ||
|
|
587b3295ae | ||
|
|
9538df65e5 | ||
|
|
6854665f68 | ||
|
|
2534afbf81 | ||
|
|
eb3d0c2874 | ||
|
|
5f85cd2873 | ||
|
|
7bb6f67dbc | ||
|
|
05cf0a0cb1 | ||
|
|
f012419710 | ||
|
|
239f771ac1 | ||
|
|
87b6715a00 | ||
|
|
d6a96317bf | ||
|
|
6d64212c93 | ||
|
|
088e746f99 | ||
|
|
c26701383e | ||
|
|
0b8f1d0168 | ||
|
|
b78337c96b | ||
|
|
9fcd327da2 | ||
|
|
c3ce69994e | ||
|
|
5bd03d1914 | ||
|
|
950780d30c | ||
|
|
8040ddd6f7 | ||
|
|
4863b2345e | ||
|
|
32b4914ba0 | ||
|
|
c76a2e807b | ||
|
|
39e7ddb781 | ||
|
|
45f08df026 | ||
|
|
09b91014da | ||
|
|
5edff0df53 | ||
|
|
617d53da43 | ||
|
|
8ba5eaf74d | ||
|
|
29f315ac33 | ||
|
|
2baac34ba3 | ||
|
|
811a1af4bf | ||
|
|
b765b46c50 | ||
|
|
6eac6affcf | ||
|
|
d2cecf6908 | ||
|
|
61d2aac45a | ||
|
|
b5d0a5e74b | ||
|
|
1c2189b63c | ||
|
|
1adb4000a6 | ||
|
|
bad978afbd | ||
|
|
80b4d6a665 | ||
|
|
f51d6542b8 | ||
|
|
7baad1c35e | ||
|
|
621bcfadd2 | ||
|
|
83bbaebef2 | ||
|
|
bde5abcdb5 | ||
|
|
99e48dbeed | ||
|
|
2ee1b26a7f | ||
|
|
a7ed8f4afe | ||
|
|
148ee3521d | ||
|
|
d7490a086a | ||
|
|
189bec58db | ||
|
|
7e26fd1040 | ||
|
|
a244808073 | ||
|
|
33f1209c80 | ||
|
|
d977c4d46b | ||
|
|
01b2f57d7a | ||
|
|
ab00d703bf | ||
|
|
6c3a7886ed | ||
|
|
5719bbcaac | ||
|
|
324d9bf780 | ||
|
|
eb8d861c6c | ||
|
|
99a4d75e4b | ||
|
|
d601484398 | ||
|
|
a3fbc8ebd8 | ||
|
|
bfc14ffd9e | ||
|
|
1c327d1433 | ||
|
|
deee914573 | ||
|
|
f6b541c30f | ||
|
|
c00fbd3652 | ||
|
|
76f056180d | ||
|
|
c39b63b944 | ||
|
|
40bf47ec0b | ||
|
|
dc2ac4bedc | ||
|
|
0cf90064ec | ||
|
|
0497660ab5 | ||
|
|
6005f7266e | ||
|
|
5d38cabe75 | ||
|
|
29f94ee6ac | ||
|
|
8929673ff0 | ||
|
|
94228c258a | ||
|
|
1b424a8ea8 | ||
|
|
35a7a79073 | ||
|
|
8dd3b2396f | ||
|
|
39d2def035 | ||
|
|
e1a9f2f761 | ||
|
|
37eb8cc0a1 | ||
|
|
402b0be80b | ||
|
|
a761590322 | ||
|
|
8d4a73cd30 | ||
|
|
4bf2b55c26 | ||
|
|
13fcbfcdc7 | ||
|
|
288a4e99fa | ||
|
|
55a3842b00 | ||
|
|
a25fb6d84d | ||
|
|
d9a2d8109e | ||
|
|
0376a5823f | ||
|
|
78e25c6958 | ||
|
|
7e5d6e99d1 | ||
|
|
f5e3a16ad1 | ||
|
|
c361387bf8 | ||
|
|
f0c6e085eb | ||
|
|
67a1bd1256 | ||
|
|
32182ae13b | ||
|
|
e56086aac3 | ||
|
|
6b142622db | ||
|
|
8d07e81b80 | ||
|
|
0fcff53eba |
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* EnerTalk Energy Meter
|
||||
*
|
||||
* Copyright 2015 hyeon seok yang
|
||||
*
|
||||
* 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: "EnerTalk Energy Meter", namespace: "Encored Technologies", author: "hyeon seok yang") {
|
||||
}
|
||||
|
||||
simulator {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
tiles(scale:2) {
|
||||
valueTile("view", "device.view", decoration: "flat") {
|
||||
state "view", label:' ${currentValue} kWh'
|
||||
}
|
||||
valueTile("month", "device.month", width: 6, height : 3, decoration: "flat") {
|
||||
state "month", label:' ${currentValue}'
|
||||
}
|
||||
valueTile("real", "device.real", width: 2, height : 2, decoration: "flat") {
|
||||
state "real", label:' ${currentValue}'
|
||||
}
|
||||
valueTile("tier", "device.tier", width: 2, height : 2, decoration: "flat") {
|
||||
state "tier", label:' ${currentValue}'
|
||||
}
|
||||
valueTile("plan", "device.plan", width: 2, height : 2, decoration: "flat") {
|
||||
state "plan", label:' ${currentValue}'
|
||||
}
|
||||
|
||||
htmlTile(name:"deepLink", action:"linkApp", whitelist:["code.jquery.com",
|
||||
"ajax.googleapis.com",
|
||||
"fonts.googleapis.com",
|
||||
"code.highcharts.com",
|
||||
"enertalk-card.encoredtech.com",
|
||||
"s3-ap-northeast-1.amazonaws.com",
|
||||
"s3.amazonaws.com",
|
||||
"ui-hub.encoredtech.com",
|
||||
"enertalk-auth.encoredtech.com",
|
||||
"api.encoredtech.com",
|
||||
"cdnjs.cloudflare.com",
|
||||
"encoredtech.com",
|
||||
"itunes.apple.com"], width:2, height:2){}
|
||||
|
||||
main (["view"])
|
||||
details (["month", "real", "tier", "plan", "deepLink"])
|
||||
}
|
||||
}
|
||||
|
||||
mappings {
|
||||
|
||||
path("/linkApp") {action: [ GET: "getLinkedApp" ]}
|
||||
}
|
||||
|
||||
def getLinkedApp() {
|
||||
def lang = clientLocale?.language
|
||||
if ("${lang}" == "ko") {
|
||||
lang = "<p style=\'margin-left:15vw; color: #aeaeb0;\'>기기 설정</p>"
|
||||
} else {
|
||||
lang = "<p style=\'margin-left:5vw; color: #aeaeb0;\'>Setup Device</p>"
|
||||
}
|
||||
renderHTML() {
|
||||
head {
|
||||
"""
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width, height=device-height">
|
||||
<style>
|
||||
#레이어_1 { margin-left : 17vw; width : 50vw; height : 50vw;}
|
||||
.st0{fill:#B5B6BB;}
|
||||
</style>
|
||||
"""
|
||||
}
|
||||
body {
|
||||
"""
|
||||
<div id="container">
|
||||
<a id="st-deep-link" href="#">
|
||||
<svg version="1.1" id="레이어_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve"><path class="st0" d="M20,0C9,0,0,9,0,20C0,30.5,8,39,18.2,40l3.8-4.8l-3.9-4.8c-4.9-0.9-8.6-5.2-8.6-10.4c0-5.8,4.7-10.5,10.5-10.5
|
||||
S30.5,14.2,30.5,20c0,5.1-3.7,9.4-8.5,10.3l3.7,4.5L21.8,40C32,39.1,40,30.5,40,20C40,9,31,0,20,0z"/></svg>
|
||||
</a>
|
||||
${lang}
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
|
||||
<script>
|
||||
var ua = navigator.userAgent.toLowerCase();
|
||||
var isAndroid = ua.indexOf("android") > -1;
|
||||
if(!isAndroid) {
|
||||
\$("#st-deep-link").attr("href", "https://itunes.apple.com/kr/app/enertalk-for-home/id1024660780?mt=8");
|
||||
} else {
|
||||
\$("#st-deep-link").attr("href", "market://details?id=com.ionicframework.enertalkhome874425");
|
||||
}
|
||||
</script>
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
// keen home smart vent
|
||||
// http://www.keenhome.io
|
||||
// SmartThings Device Handler v1.0.0
|
||||
|
||||
metadata {
|
||||
definition (name: "Keen Home Smart Vent", namespace: "Keen Home", author: "Keen Home") {
|
||||
capability "Switch Level"
|
||||
capability "Switch"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Temperature Measurement"
|
||||
capability "Battery"
|
||||
|
||||
command "getLevel"
|
||||
command "getOnOff"
|
||||
command "getPressure"
|
||||
command "getBattery"
|
||||
command "getTemperature"
|
||||
command "setZigBeeIdTile"
|
||||
command "clearObstruction"
|
||||
|
||||
fingerprint endpoint: "1",
|
||||
profileId: "0104",
|
||||
inClusters: "0000,0001,0003,0004,0005,0006,0008,0020,0402,0403,0B05,FC01,FC02",
|
||||
outClusters: "0019"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
// status messages
|
||||
status "on": "on/off: 1"
|
||||
status "off": "on/off: 0"
|
||||
|
||||
// reply messages
|
||||
reply "zcl on-off on": "on/off: 1"
|
||||
reply "zcl on-off off": "on/off: 0"
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles {
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on", action: "switch.off", icon: "st.vents.vent-open-text", backgroundColor: "#53a7c0"
|
||||
state "off", action: "switch.on", icon: "st.vents.vent-closed", backgroundColor: "#ffffff"
|
||||
state "obstructed", action: "clearObstruction", icon: "st.vents.vent-closed", backgroundColor: "#ff0000"
|
||||
state "clearing", action: "", icon: "st.vents.vent-closed", backgroundColor: "#ffff33"
|
||||
}
|
||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
valueTile("temperature", "device.temperature", inactiveLabel: false) {
|
||||
state "temperature", label:'${currentValue}°',
|
||||
backgroundColors:[
|
||||
[value: 31, color: "#153591"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
}
|
||||
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") {
|
||||
state "battery", label: 'Battery \n${currentValue}%', backgroundColor:"#ffffff"
|
||||
}
|
||||
valueTile("zigbeeId", "device.zigbeeId", inactiveLabel: true, decoration: "flat") {
|
||||
state "serial", label:'${currentValue}', backgroundColor:"#ffffff"
|
||||
}
|
||||
main "switch"
|
||||
details(["switch","refresh","temperature","levelSliderControl","battery"])
|
||||
}
|
||||
}
|
||||
|
||||
/**** PARSE METHODS ****/
|
||||
def parse(String description) {
|
||||
log.debug "description: $description"
|
||||
|
||||
Map map = [:]
|
||||
if (description?.startsWith('catchall:')) {
|
||||
map = parseCatchAllMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('read attr -')) {
|
||||
map = parseReportAttributeMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('temperature: ') || description?.startsWith('humidity: ')) {
|
||||
map = parseCustomMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('on/off: ')) {
|
||||
map = parseOnOffMessage(description)
|
||||
}
|
||||
|
||||
log.debug "Parse returned $map"
|
||||
return map ? createEvent(map) : null
|
||||
}
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
log.debug "parseCatchAllMessage"
|
||||
|
||||
def cluster = zigbee.parse(description)
|
||||
log.debug "cluster: ${cluster}"
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
log.debug "processing message"
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0001:
|
||||
return makeBatteryResult(cluster.data.last())
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = convertTemperatureHex(temp)
|
||||
return makeTemperatureResult(value)
|
||||
break
|
||||
|
||||
case 0x0006:
|
||||
return makeOnOffResult(cluster.data[-1])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return [:]
|
||||
}
|
||||
|
||||
private boolean shouldProcessMessage(cluster) {
|
||||
// 0x0B is default response indicating message got through
|
||||
// 0x07 is bind message
|
||||
if (cluster.profileId != 0x0104 ||
|
||||
cluster.command == 0x0B ||
|
||||
cluster.command == 0x07 ||
|
||||
(cluster.data.size() > 0 && cluster.data.first() == 0x3e)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private Map parseReportAttributeMessage(String description) {
|
||||
log.debug "parseReportAttributeMessage"
|
||||
|
||||
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
log.debug "Desc Map: $descMap"
|
||||
|
||||
if (descMap.cluster == "0006" && descMap.attrId == "0000") {
|
||||
return makeOnOffResult(Int.parseInt(descMap.value));
|
||||
}
|
||||
else if (descMap.cluster == "0008" && descMap.attrId == "0000") {
|
||||
return makeLevelResult(descMap.value)
|
||||
}
|
||||
else if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
||||
def value = convertTemperatureHex(descMap.value)
|
||||
return makeTemperatureResult(value)
|
||||
}
|
||||
else if (descMap.cluster == "0001" && descMap.attrId == "0021") {
|
||||
return makeBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||
}
|
||||
else if (descMap.cluster == "0403" && descMap.attrId == "0020") {
|
||||
return makePressureResult(Integer.parseInt(descMap.value, 16))
|
||||
}
|
||||
else if (descMap.cluster == "0000" && descMap.attrId == "0006") {
|
||||
return makeSerialResult(new String(descMap.value.decodeHex()))
|
||||
}
|
||||
|
||||
// shouldn't get here
|
||||
return [:]
|
||||
}
|
||||
|
||||
private Map parseCustomMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
if (description?.startsWith('temperature: ')) {
|
||||
// log.debug "${description}"
|
||||
// def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale())
|
||||
// log.debug "split: " + description.split(": ")
|
||||
def value = Double.parseDouble(description.split(": ")[1])
|
||||
// log.debug "${value}"
|
||||
resultMap = makeTemperatureResult(convertTemperature(value))
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private Map parseOnOffMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
if (description?.startsWith('on/off: ')) {
|
||||
def value = Integer.parseInt(description - "on/off: ")
|
||||
resultMap = makeOnOffResult(value)
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
private Map makeOnOffResult(rawValue) {
|
||||
log.debug "makeOnOffResult: ${rawValue}"
|
||||
def linkText = getLinkText(device)
|
||||
def value = rawValue == 1 ? "on" : "off"
|
||||
return [
|
||||
name: "switch",
|
||||
value: value,
|
||||
descriptionText: "${linkText} is ${value}"
|
||||
]
|
||||
}
|
||||
|
||||
private Map makeLevelResult(rawValue) {
|
||||
def linkText = getLinkText(device)
|
||||
def value = Integer.parseInt(rawValue, 16)
|
||||
def rangeMax = 254
|
||||
|
||||
// catch obstruction level
|
||||
if (value == 255) {
|
||||
log.debug "${linkText} is obstructed"
|
||||
// Just return here. Once the vent is power cycled
|
||||
// it will go back to the previous level before obstruction.
|
||||
// Therefore, no need to update level on the display.
|
||||
return [
|
||||
name: "switch",
|
||||
value: "obstructed",
|
||||
descriptionText: "${linkText} is obstructed. Please power cycle."
|
||||
]
|
||||
}
|
||||
|
||||
value = Math.floor(value / rangeMax * 100)
|
||||
|
||||
return [
|
||||
name: "level",
|
||||
value: value,
|
||||
descriptionText: "${linkText} level is ${value}%"
|
||||
]
|
||||
}
|
||||
|
||||
private Map makePressureResult(rawValue) {
|
||||
log.debug 'makePressureResut'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
def pascals = rawValue / 10
|
||||
def result = [
|
||||
name: 'pressure',
|
||||
descriptionText: "${linkText} pressure is ${pascals}Pa",
|
||||
value: pascals
|
||||
]
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private Map makeBatteryResult(rawValue) {
|
||||
// log.debug 'makeBatteryResult'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
// log.debug
|
||||
[
|
||||
name: 'battery',
|
||||
value: rawValue,
|
||||
descriptionText: "${linkText} battery is at ${rawValue}%"
|
||||
]
|
||||
}
|
||||
|
||||
private Map makeTemperatureResult(value) {
|
||||
// log.debug 'makeTemperatureResult'
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
// log.debug "tempOffset: ${tempOffset}"
|
||||
if (tempOffset) {
|
||||
def offset = tempOffset as int
|
||||
// log.debug "offset: ${offset}"
|
||||
def v = value as int
|
||||
// log.debug "v: ${v}"
|
||||
value = v + offset
|
||||
// log.debug "value: ${value}"
|
||||
}
|
||||
|
||||
return [
|
||||
name: 'temperature',
|
||||
value: "" + value,
|
||||
descriptionText: "${linkText} is ${value}°${temperatureScale}",
|
||||
]
|
||||
}
|
||||
|
||||
/**** HELPER METHODS ****/
|
||||
private def convertTemperatureHex(value) {
|
||||
// log.debug "convertTemperatureHex(${value})"
|
||||
def celsius = Integer.parseInt(value, 16).shortValue() / 100
|
||||
// log.debug "celsius: ${celsius}"
|
||||
|
||||
return convertTemperature(celsius)
|
||||
}
|
||||
|
||||
private def convertTemperature(celsius) {
|
||||
// log.debug "convertTemperature()"
|
||||
|
||||
if(getTemperatureScale() == "C"){
|
||||
return celsius
|
||||
} else {
|
||||
def fahrenheit = Math.round(celsiusToFahrenheit(celsius) * 100) /100
|
||||
// log.debug "converted to F: ${fahrenheit}"
|
||||
return fahrenheit
|
||||
}
|
||||
}
|
||||
|
||||
private def makeSerialResult(serial) {
|
||||
log.debug "makeSerialResult: " + serial
|
||||
|
||||
def linkText = getLinkText(device)
|
||||
sendEvent([
|
||||
name: "serial",
|
||||
value: serial,
|
||||
descriptionText: "${linkText} has serial ${serial}" ])
|
||||
return [
|
||||
name: "serial",
|
||||
value: serial,
|
||||
descriptionText: "${linkText} has serial ${serial}" ]
|
||||
}
|
||||
|
||||
// takes a level from 0 to 100 and translates it to a ZigBee move to level with on/off command
|
||||
private def makeLevelCommand(level) {
|
||||
def rangeMax = 254
|
||||
def scaledLevel = Math.round(level * rangeMax / 100)
|
||||
log.debug "scaled level for ${level}%: ${scaledLevel}"
|
||||
|
||||
// convert to hex string and pad to two digits
|
||||
def hexLevel = new BigInteger(scaledLevel.toString()).toString(16).padLeft(2, '0')
|
||||
|
||||
"st cmd 0x${device.deviceNetworkId} 1 8 4 {${hexLevel} 0000}"
|
||||
}
|
||||
|
||||
/**** COMMAND METHODS ****/
|
||||
def on() {
|
||||
def linkText = getLinkText(device)
|
||||
log.debug "open ${linkText}"
|
||||
|
||||
// only change the state if the vent is not obstructed
|
||||
if (device.currentValue("switch") == "obstructed") {
|
||||
log.error("cannot open because ${linkText} is obstructed")
|
||||
return
|
||||
}
|
||||
|
||||
sendEvent(makeOnOffResult(1))
|
||||
"st cmd 0x${device.deviceNetworkId} 1 6 1 {}"
|
||||
}
|
||||
|
||||
def off() {
|
||||
def linkText = getLinkText(device)
|
||||
log.debug "close ${linkText}"
|
||||
|
||||
// only change the state if the vent is not obstructed
|
||||
if (device.currentValue("switch") == "obstructed") {
|
||||
log.error("cannot close because ${linkText} is obstructed")
|
||||
return
|
||||
}
|
||||
|
||||
sendEvent(makeOnOffResult(0))
|
||||
"st cmd 0x${device.deviceNetworkId} 1 6 0 {}"
|
||||
}
|
||||
|
||||
def clearObstruction() {
|
||||
def linkText = getLinkText(device)
|
||||
log.debug "attempting to clear ${linkText} obstruction"
|
||||
|
||||
sendEvent([
|
||||
name: "switch",
|
||||
value: "clearing",
|
||||
descriptionText: "${linkText} is clearing obstruction"
|
||||
])
|
||||
|
||||
// send a move command to ensure level attribute gets reset for old, buggy firmware
|
||||
// then send a reset to factory defaults
|
||||
// finally re-configure to ensure reports and binding is still properly set after the rtfd
|
||||
[
|
||||
makeLevelCommand(device.currentValue("level")), "delay 500",
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0 0 {}", "delay 5000"
|
||||
] + configure()
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
log.debug "setting level: ${value}"
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
// only change the level if the vent is not obstructed
|
||||
def currentState = device.currentValue("switch")
|
||||
|
||||
if (currentState == "obstructed") {
|
||||
log.error("cannot set level because ${linkText} is obstructed")
|
||||
return
|
||||
}
|
||||
|
||||
sendEvent(name: "level", value: value)
|
||||
if (value > 0) {
|
||||
sendEvent(name: "switch", value: "on", descriptionText: "${linkText} is on by setting a level")
|
||||
}
|
||||
else {
|
||||
sendEvent(name: "switch", value: "off", descriptionText: "${linkText} is off by setting level to 0")
|
||||
}
|
||||
|
||||
makeLevelCommand(value)
|
||||
}
|
||||
|
||||
def getOnOff() {
|
||||
log.debug "getOnOff()"
|
||||
|
||||
// disallow on/off updates while vent is obstructed
|
||||
if (device.currentValue("switch") == "obstructed") {
|
||||
log.error("cannot update open/close status because ${getLinkText(device)} is obstructed")
|
||||
return []
|
||||
}
|
||||
|
||||
["st rattr 0x${device.deviceNetworkId} 1 0x0006 0"]
|
||||
}
|
||||
|
||||
def getPressure() {
|
||||
log.debug "getPressure()"
|
||||
|
||||
// using a Keen Home specific attribute in the pressure measurement cluster
|
||||
[
|
||||
"zcl mfg-code 0x115B", "delay 200",
|
||||
"zcl global read 0x0403 0x20", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
|
||||
]
|
||||
}
|
||||
|
||||
def getLevel() {
|
||||
log.debug "getLevel()"
|
||||
|
||||
// disallow level updates while vent is obstructed
|
||||
if (device.currentValue("switch") == "obstructed") {
|
||||
log.error("cannot update level status because ${getLinkText(device)} is obstructed")
|
||||
return []
|
||||
}
|
||||
|
||||
["st rattr 0x${device.deviceNetworkId} 1 0x0008 0x0000"]
|
||||
}
|
||||
|
||||
def getTemperature() {
|
||||
log.debug "getTemperature()"
|
||||
|
||||
["st rattr 0x${device.deviceNetworkId} 1 0x0402 0"]
|
||||
}
|
||||
|
||||
def getBattery() {
|
||||
log.debug "getBattery()"
|
||||
|
||||
["st rattr 0x${device.deviceNetworkId} 1 0x0001 0x0021"]
|
||||
}
|
||||
|
||||
def setZigBeeIdTile() {
|
||||
log.debug "setZigBeeIdTile() - ${device.zigbeeId}"
|
||||
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
sendEvent([
|
||||
name: "zigbeeId",
|
||||
value: device.zigbeeId,
|
||||
descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ])
|
||||
return [
|
||||
name: "zigbeeId",
|
||||
value: device.zigbeeId,
|
||||
descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
getOnOff() +
|
||||
getLevel() +
|
||||
getTemperature() +
|
||||
getPressure() +
|
||||
getBattery()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "CONFIGURE"
|
||||
|
||||
// get ZigBee ID by hidden tile because that's the only way we can do it
|
||||
setZigBeeIdTile()
|
||||
|
||||
def configCmds = [
|
||||
// bind reporting clusters to hub
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0006 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0008 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0403 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0001 {${device.zigbeeId}} {}", "delay 500"
|
||||
|
||||
// configure report commands
|
||||
// zcl global send-me-a-report [cluster] [attr] [type] [min-interval] [max-interval] [min-change]
|
||||
|
||||
// report with these parameters is preconfigured in firmware, can be overridden here
|
||||
// vent on/off state - type: boolean, change: 1
|
||||
// "zcl global send-me-a-report 6 0 0x10 5 60 {01}", "delay 200",
|
||||
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
// report with these parameters is preconfigured in firmware, can be overridden here
|
||||
// vent level - type: int8u, change: 1
|
||||
// "zcl global send-me-a-report 8 0 0x20 5 60 {01}", "delay 200",
|
||||
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
// report with these parameters is preconfigured in firmware, can be overridden here
|
||||
// temperature - type: int16s, change: 0xA = 10 = 0.1C
|
||||
// "zcl global send-me-a-report 0x0402 0 0x29 60 60 {0A00}", "delay 200",
|
||||
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
// report with these parameters is preconfigured in firmware, can be overridden here
|
||||
// keen home custom pressure (tenths of Pascals) - type: int32u, change: 1 = 0.1Pa
|
||||
// "zcl mfg-code 0x115B", "delay 200",
|
||||
// "zcl global send-me-a-report 0x0403 0x20 0x22 60 60 {010000}", "delay 200",
|
||||
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
// report with these parameters is preconfigured in firmware, can be overridden here
|
||||
// battery - type: int8u, change: 1
|
||||
// "zcl global send-me-a-report 1 0x21 0x20 60 3600 {01}", "delay 200",
|
||||
// "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
]
|
||||
|
||||
return configCmds + refresh()
|
||||
}
|
||||
163
devicetypes/osotech/plantlink.src/plantlink.groovy
Normal file
163
devicetypes/osotech/plantlink.src/plantlink.groovy
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* PlantLink
|
||||
*
|
||||
* This device type takes sensor data and converts it into a json packet to send to myplantlink.com
|
||||
* where its values will be computed for soil and plant type to show user readable values of how your
|
||||
* specific plant is doing.
|
||||
*
|
||||
*
|
||||
* Copyright 2015 Oso Technologies
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
import groovy.json.JsonBuilder
|
||||
|
||||
metadata {
|
||||
definition (name: "PlantLink", namespace: "OsoTech", author: "Oso Technologies") {
|
||||
capability "Sensor"
|
||||
|
||||
command "setStatusIcon"
|
||||
command "setPlantFuelLevel"
|
||||
command "setBatteryLevel"
|
||||
command "setInstallSmartApp"
|
||||
|
||||
attribute "plantStatus","string"
|
||||
attribute "plantFuelLevel","number"
|
||||
attribute "linkBatteryLevel","string"
|
||||
attribute "installSmartApp","string"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0B04"
|
||||
}
|
||||
|
||||
simulator {
|
||||
status "battery": "read attr - raw: C0720100010A000021340A, dni: C072, endpoint: 01, cluster: 0001, size: 0A, attrId: 0000, encoding: 21, value: 0a34"
|
||||
status "moisture": "read attr - raw: C072010B040A0001290000, dni: C072, endpoint: 01, cluster: 0B04, size: 0A, attrId: 0100, encoding: 29, value: 0000"
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("Title", "device.label") {
|
||||
state("label", label:'PlantLink ${device.label}')
|
||||
}
|
||||
|
||||
valueTile("plantMoistureTile", "device.plantFuelLevel", width: 1, height: 1) {
|
||||
state("plantMoisture", label: '${currentValue}% Moisture')
|
||||
}
|
||||
|
||||
valueTile("plantStatusTextTile", "device.plantStatus", decoration: "flat", width: 2, height: 2) {
|
||||
state("plantStatusTextTile", label:'${currentValue}')
|
||||
}
|
||||
|
||||
valueTile("battery", "device.linkBatteryLevel" ) {
|
||||
state("battery", label:'${currentValue}% battery')
|
||||
}
|
||||
|
||||
valueTile("installSmartApp","device.installSmartApp", decoration: "flat", width: 3, height: 1) {
|
||||
state "needSmartApp", label:'Please install SmartApp "Required PlantLink Connector"', defaultState:true
|
||||
state "connectedToSmartApp", label:'Connected to myplantlink.com'
|
||||
}
|
||||
|
||||
main "plantStatusTextTile"
|
||||
details(['plantStatusTextTile', "plantMoistureTile", "battery", "installSmartApp"])
|
||||
}
|
||||
}
|
||||
|
||||
def setStatusIcon(value){
|
||||
def status = ''
|
||||
switch (value) {
|
||||
case '0':
|
||||
status = 'Needs Water'
|
||||
break
|
||||
case '1':
|
||||
status = 'Dry'
|
||||
break
|
||||
case '2':
|
||||
case '3':
|
||||
status = 'Good'
|
||||
break
|
||||
case '4':
|
||||
status = 'Too Wet'
|
||||
break
|
||||
case 'No Soil':
|
||||
status = 'Too Dry'
|
||||
setPlantFuelLevel(0)
|
||||
break
|
||||
case 'Recently Watered':
|
||||
status = 'Watered'
|
||||
setPlantFuelLevel(100)
|
||||
break
|
||||
case 'Low Battery':
|
||||
status = 'Low Battery'
|
||||
break
|
||||
case 'Waiting on First Measurement':
|
||||
status = 'Calibrating'
|
||||
break
|
||||
default:
|
||||
status = "?"
|
||||
break
|
||||
}
|
||||
sendEvent("name":"plantStatus", "value":status, "description":statusText, displayed: true, isStateChange: true)
|
||||
}
|
||||
|
||||
def setPlantFuelLevel(value){
|
||||
sendEvent("name":"plantFuelLevel", "value":value, "description":statusText, displayed: true, isStateChange: true)
|
||||
}
|
||||
|
||||
def setBatteryLevel(value){
|
||||
sendEvent("name":"linkBatteryLevel", "value":value, "description":statusText, displayed: true, isStateChange: true)
|
||||
}
|
||||
|
||||
def setInstallSmartApp(value){
|
||||
sendEvent("name":"installSmartApp", "value":value)
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
|
||||
def description_map = parseDescriptionAsMap(description)
|
||||
def event_name = ""
|
||||
def measurement_map = [
|
||||
type: "link",
|
||||
signal: "0x00",
|
||||
zigbeedeviceid: device.zigbeeId,
|
||||
created: new Date().time /1000 as int
|
||||
]
|
||||
if (description_map.cluster == "0000"){
|
||||
/* version number, not used */
|
||||
|
||||
} else if (description_map.cluster == "0001"){
|
||||
/* battery voltage in mV (device needs minimium 2.1v to run) */
|
||||
log.debug "PlantLink - id ${device.zigbeeId} battery ${description_map.value}"
|
||||
event_name = "battery_status"
|
||||
measurement_map["battery"] = "0x${description_map.value}"
|
||||
|
||||
} else if (description_map.cluster == "0B04"){
|
||||
/* raw moisture reading (needs to be sent to plantlink for soil/plant type conversion) */
|
||||
log.debug "PlantLink - id ${device.zigbeeId} raw moisture ${description_map.value}"
|
||||
measurement_map["moisture"] = "0x${description_map.value}"
|
||||
event_name = "moisture_status"
|
||||
|
||||
} else{
|
||||
log.debug "PlantLink - id ${device.zigbeeId} Unknown '${description}'"
|
||||
return
|
||||
}
|
||||
|
||||
def json_builder = new JsonBuilder(measurement_map)
|
||||
def result = createEvent(name: event_name, value: json_builder.toString())
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ metadata {
|
||||
state "configure", label: '', action: "configuration.configure", icon: "st.secondary.configure"
|
||||
}
|
||||
|
||||
PLATFORM_graphTile(name: "powerGraph", attribute: "device.power")
|
||||
graphTile(name: "powerGraph", attribute: "device.power")
|
||||
|
||||
main(["power", "energy"])
|
||||
details(["powerGraph", "power", "energy", "reset", "refresh", "configure"])
|
||||
@@ -68,16 +68,8 @@ metadata {
|
||||
// ========================================================
|
||||
|
||||
preferences {
|
||||
input name: "graphPrecision", type: "enum", title: "Graph Precision", description: "Daily", required: true, options: PLATFORM_graphPrecisionOptions(), defaultValue: "Daily"
|
||||
input name: "graphType", type: "enum", title: "Graph Type", description: selectedGraphType(), required: false, options: PLATFORM_graphTypeOptions()
|
||||
}
|
||||
|
||||
def selectedGraphPrecision() {
|
||||
graphPrecision ?: "Daily"
|
||||
}
|
||||
|
||||
def selectedGraphType() {
|
||||
graphType ?: "line"
|
||||
input name: "graphPrecision", type: "enum", title: "Graph Precision", description: "Daily", required: true, options: graphPrecisionOptions(), defaultValue: "Daily"
|
||||
input name: "graphType", type: "enum", title: "Graph Type", description: "line", required: false, options: graphTypeOptions()
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
@@ -91,22 +83,6 @@ mappings {
|
||||
GET: "renderGraph"
|
||||
]
|
||||
}
|
||||
path("/graphDataSizes") { // for testing. remove before publishing
|
||||
action:
|
||||
[
|
||||
GET: "graphDataSizes"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def graphDataSizes() { // for testing. remove before publishing
|
||||
state.findAll { k, v -> k.startsWith("measure.") }.inject([:]) { attributes, attributeData ->
|
||||
attributes[attributeData.key] = attributeData.value.inject([:]) { dateTypes, dateTypeData ->
|
||||
dateTypes[dateTypeData.key] = dateTypeData.value.size()
|
||||
dateTypes
|
||||
}
|
||||
attributes
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
@@ -121,8 +97,7 @@ def parse(String description) {
|
||||
}
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
|
||||
PLATFORM_migrateGraphDataIfNeeded()
|
||||
PLATFORM_storeData(result.name, result.value)
|
||||
storeGraphData(result.name, result.value)
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -176,535 +151,15 @@ def configure() {
|
||||
|
||||
def renderGraph() {
|
||||
|
||||
def data = PLATFORM_fetchGraphData(params.attribute)
|
||||
def data = fetchGraphData(params.attribute)
|
||||
|
||||
def totalData = data*.runningSum
|
||||
|
||||
def xValues = data*.unixTime
|
||||
|
||||
def yValues = [
|
||||
Total: [color: "#49a201", data: totalData, type: selectedGraphType()]
|
||||
Total: [color: "#49a201", data: totalData]
|
||||
]
|
||||
|
||||
PLATFORM_renderGraph(attribute: params.attribute, xValues: xValues, yValues: yValues, focus: "Total", label: "Watts")
|
||||
}
|
||||
|
||||
// TODO: // ========================================================
|
||||
// TODO: // PLATFORM CODE !!! DO NOT ALTER !!!
|
||||
// TODO: // ========================================================
|
||||
|
||||
// ========================================================
|
||||
// PLATFORM TILES
|
||||
// ========================================================
|
||||
|
||||
def PLATFORM_graphTile(Map tileParams) {
|
||||
def cleanAttribute = tileParams.attribute - "device." - "capability."
|
||||
htmlTile([name: tileParams.name, attribute: tileParams.attribute, action: "graph/${cleanAttribute}", width: 3, height: 2] + tileParams)
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// PLATFORM GRAPH RENDERING
|
||||
// ========================================================
|
||||
|
||||
private PLATFORM_graphTypeOptions() {
|
||||
[
|
||||
"line", // DEFAULT
|
||||
"spline",
|
||||
"step",
|
||||
"area",
|
||||
"area-spline",
|
||||
"area-step",
|
||||
"bar",
|
||||
"scatter",
|
||||
"pie",
|
||||
"donut",
|
||||
"gauge",
|
||||
]
|
||||
}
|
||||
|
||||
private PLATFORM_renderGraph(graphParams) {
|
||||
|
||||
String attribute = graphParams.attribute
|
||||
List xValues = graphParams.xValues
|
||||
Map yValues = graphParams.yValues
|
||||
String focus = graphParams.focus ?: ""
|
||||
String label = graphParams.label ?: ""
|
||||
|
||||
/*
|
||||
def xValues = [1, 2]
|
||||
|
||||
def yValues = [
|
||||
High: [type: "spline", data: [5, 6], color: "#bc2323"],
|
||||
Low: [type: "spline", data: [0, 1], color: "#153591"]
|
||||
]
|
||||
|
||||
Available type values:
|
||||
line // DEFAULT
|
||||
spline
|
||||
step
|
||||
area
|
||||
area-spline
|
||||
area-step
|
||||
bar
|
||||
scatter
|
||||
pie
|
||||
donut
|
||||
gauge
|
||||
|
||||
*/
|
||||
|
||||
def graphData = PLATFORM_buildGraphData(xValues, yValues, label)
|
||||
|
||||
def legendData = yValues*.key
|
||||
def focusJS = focus ? "chart.focus('${focus}')" : "// focus not specified"
|
||||
def flowColumn = focus ?: yValues ? yValues.keySet().first() : null
|
||||
|
||||
def htmlTitle = "${(device.label ?: device.name)} ${attribute.capitalize()} Graph"
|
||||
renderHTML(htmlTitle) { html ->
|
||||
html.head {
|
||||
"""
|
||||
<!-- Load c3.css -->
|
||||
<link href="https://www.dropbox.com/s/m6ptp72cw4nx0sp/c3.css?dl=1" rel="stylesheet" type="text/css">
|
||||
|
||||
<!-- Load d3.js and c3.js -->
|
||||
<script src="https://www.dropbox.com/s/9x22jyfu5qyacpp/d3.v3.min.js?dl=1" charset="utf-8"></script>
|
||||
<script src="https://www.dropbox.com/s/to7dtcn403l7mza/c3.js?dl=1"></script>
|
||||
|
||||
<script>
|
||||
function getDocumentHeight() {
|
||||
var body = document.body;
|
||||
var html = document.documentElement;
|
||||
|
||||
return html.clientHeight;
|
||||
}
|
||||
function getDocumentWidth() {
|
||||
var body = document.body;
|
||||
var html = document.documentElement;
|
||||
|
||||
return html.clientWidth;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.legend {
|
||||
position: absolute;
|
||||
width: 80%;
|
||||
padding-left: 15%;
|
||||
z-index: 999;
|
||||
padding-top: 5px;
|
||||
}
|
||||
.legend span {
|
||||
width: ${100 / yValues.size()}%;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
}
|
||||
html.body {
|
||||
"""
|
||||
<div class="legend"></div>
|
||||
<div id="chart" style="max-height: 120px; position: relative;"></div>
|
||||
|
||||
<script>
|
||||
|
||||
// Generate the chart
|
||||
var chart = c3.generate(${graphData as grails.converters.JSON});
|
||||
|
||||
// Resize the chart to the size of the device tile
|
||||
chart.resize({height:getDocumentHeight(), width:getDocumentWidth()});
|
||||
|
||||
// Focus data if specified
|
||||
${focusJS}
|
||||
|
||||
// Update the chart when ${attribute} events are received
|
||||
function ${attribute}(evt) {
|
||||
var newValue = ['${flowColumn}'];
|
||||
newValue.push(evt.value);
|
||||
|
||||
var newX = ['x'];
|
||||
newX.push(evt.unixTime);
|
||||
|
||||
chart.flow({
|
||||
columns: [
|
||||
newX,
|
||||
newValue
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// Build the custom legend
|
||||
d3.select('.legend').selectAll('span')
|
||||
.data(${legendData as grails.converters.JSON})
|
||||
.enter().append('span')
|
||||
.attr('data-id', function (id) { return id; })
|
||||
.html(function (id) { return id; })
|
||||
.each(function (id) {
|
||||
d3.select(this).style('background-color', chart.color(id));
|
||||
})
|
||||
.on('mouseover', function (id) {
|
||||
chart.focus(id);
|
||||
})
|
||||
.on('mouseout', function (id) {
|
||||
chart.revert();
|
||||
})
|
||||
.on('click', function (id) {
|
||||
chart.toggle(id);
|
||||
});
|
||||
|
||||
</script>
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PLATFORM_buildGraphData(List xValues, Map yValues, String label = "") {
|
||||
|
||||
/*
|
||||
def xValues = [1, 2]
|
||||
|
||||
def yValues = [
|
||||
High: [type: "spline", data: [5, 6], color: "#bc2323"],
|
||||
Low: [type: "spline", data: [0, 1], color: "#153591"]
|
||||
]
|
||||
*/
|
||||
|
||||
[
|
||||
interaction: [
|
||||
enabled: false
|
||||
],
|
||||
bindto : '#chart',
|
||||
padding : [
|
||||
left : 30,
|
||||
right : 30,
|
||||
bottom: 0,
|
||||
top : 0
|
||||
],
|
||||
legend : [
|
||||
show: false,
|
||||
// hide : false,//(yValues.keySet().size() < 2),
|
||||
// position: 'inset',
|
||||
// inset: [
|
||||
// anchor: "top-right"
|
||||
// ],
|
||||
// item: [
|
||||
// onclick: "do nothing" // (yValues.keySet().size() > 1) ? null : "do nothing"
|
||||
// ]
|
||||
],
|
||||
data : [
|
||||
x : "x",
|
||||
columns: [(["x"] + xValues)] + yValues.collect { k, v -> [k] + v.data },
|
||||
types : yValues.inject([:]) { total, current -> total[current.key] = current.value.type; return total },
|
||||
colors : yValues.inject([:]) { total, current -> total[current.key] = current.value.color; return total }
|
||||
],
|
||||
axis : [
|
||||
x: [
|
||||
type: 'timeseries',
|
||||
tick: [
|
||||
centered: true,
|
||||
culling : [max: 7],
|
||||
fit : true,
|
||||
format : PLATFORM_getGraphDateFormat()
|
||||
// format: PLATFORM_getGraphDateFormatFunction() // throws securityException when trying to escape javascript
|
||||
]
|
||||
],
|
||||
y: [
|
||||
label : label,
|
||||
padding: [
|
||||
top: 50
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
private PLATFORM_getGraphDateFormat(dateType = selectedGraphPrecision()) {
|
||||
// https://github.com/mbostock/d3/wiki/Time-Formatting
|
||||
def graphDateFormat
|
||||
switch (dateType) {
|
||||
case "Live":
|
||||
graphDateFormat = "%I:%M" // hour (12-hour clock) as a decimal number [00,12] // AM or PM
|
||||
break
|
||||
case "Hourly":
|
||||
graphDateFormat = "%I %p" // hour (12-hour clock) as a decimal number [00,12] // AM or PM
|
||||
break
|
||||
case "Daily":
|
||||
graphDateFormat = "%a" // abbreviated weekday name
|
||||
break
|
||||
case "Monthly":
|
||||
graphDateFormat = "%b" // abbreviated month name
|
||||
break
|
||||
case "Annually":
|
||||
graphDateFormat = "%y" // year without century as a decimal number [00,99]
|
||||
break
|
||||
}
|
||||
graphDateFormat
|
||||
}
|
||||
|
||||
private String PLATFORM_getGraphDateFormatFunction(dateType = selectedGraphPrecision()) {
|
||||
def graphDateFunction = "function(date) { return date; }"
|
||||
switch (dateType) {
|
||||
case "Live":
|
||||
graphDateFunction = """
|
||||
function(date) {
|
||||
return.getMinutes();
|
||||
}
|
||||
"""
|
||||
break;
|
||||
case "Hourly":
|
||||
graphDateFunction = """ function(date) {
|
||||
var hour = date.getHours();
|
||||
if (hour == 0) {
|
||||
return String(/12 am/).substring(1).slice(0,-1);
|
||||
} else if (hour > 12) {
|
||||
return hour -12 + String(/ pm/).substring(1).slice(0,-1);
|
||||
} else {
|
||||
return hour + String(/ am/).substring(1).slice(0,-1);
|
||||
}
|
||||
}"""
|
||||
break
|
||||
case "Daily":
|
||||
graphDateFunction = """ function(date) {
|
||||
var day = date.getDay();
|
||||
switch(day) {
|
||||
case 0: return String(/Sun/).substring(1).slice(0,-1);
|
||||
case 1: return String(/Mon/).substring(1).slice(0,-1);
|
||||
case 2: return String(/Tue/).substring(1).slice(0,-1);
|
||||
case 3: return String(/Wed/).substring(1).slice(0,-1);
|
||||
case 4: return String(/Thu/).substring(1).slice(0,-1);
|
||||
case 5: return String(/Fri/).substring(1).slice(0,-1);
|
||||
case 6: return String(/Sat/).substring(1).slice(0,-1);
|
||||
}
|
||||
}"""
|
||||
break
|
||||
case "Monthly":
|
||||
graphDateFunction = """ function(date) {
|
||||
var month = date.getMonth();
|
||||
switch(month) {
|
||||
case 0: return String(/Jan/).substring(1).slice(0,-1);
|
||||
case 1: return String(/Feb/).substring(1).slice(0,-1);
|
||||
case 2: return String(/Mar/).substring(1).slice(0,-1);
|
||||
case 3: return String(/Apr/).substring(1).slice(0,-1);
|
||||
case 4: return String(/May/).substring(1).slice(0,-1);
|
||||
case 5: return String(/Jun/).substring(1).slice(0,-1);
|
||||
case 6: return String(/Jul/).substring(1).slice(0,-1);
|
||||
case 7: return String(/Aug/).substring(1).slice(0,-1);
|
||||
case 8: return String(/Sep/).substring(1).slice(0,-1);
|
||||
case 9: return String(/Oct/).substring(1).slice(0,-1);
|
||||
case 10: return String(/Nov/).substring(1).slice(0,-1);
|
||||
case 11: return String(/Dec/).substring(1).slice(0,-1);
|
||||
}
|
||||
}"""
|
||||
break
|
||||
case "Annually":
|
||||
graphDateFunction = """
|
||||
function(date) {
|
||||
return.getFullYear();
|
||||
}
|
||||
"""
|
||||
break
|
||||
}
|
||||
groovy.json.StringEscapeUtils.escapeJavaScript(graphDateFunction)
|
||||
}
|
||||
|
||||
private jsEscapeString(str = "") {
|
||||
"String(/${str}/).substring(1).slice(0,-1);"
|
||||
}
|
||||
|
||||
private PLATFORM_fetchGraphData(attribute) {
|
||||
|
||||
log.debug "PLATFORM_fetchGraphData(${attribute})"
|
||||
|
||||
/*
|
||||
[
|
||||
[
|
||||
dateString: "2014-12-1",
|
||||
unixTime: 1421931600000,
|
||||
min: 0,
|
||||
max: 10,
|
||||
average: 5
|
||||
],
|
||||
...
|
||||
]
|
||||
*/
|
||||
|
||||
def attributeBucket = state["measure.${attribute}"] ?: [:]
|
||||
def dateType = selectedGraphPrecision()
|
||||
attributeBucket[dateType]
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// PLATFORM DATA STORAGE
|
||||
// ========================================================
|
||||
|
||||
private PLATFORM_graphPrecisionOptions() { ["Live", "Hourly", "Daily", "Monthly", "Annually"] }
|
||||
|
||||
private PLATFORM_storeData(attribute, value) {
|
||||
PLATFORM_graphPrecisionOptions().each { dateType ->
|
||||
PLATFORM_addDataToBucket(attribute, value, dateType)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
[
|
||||
Hourly: [
|
||||
[
|
||||
dateString: "2014-12-1",
|
||||
unixTime: 1421931600000,
|
||||
min: 0,
|
||||
max: 10,
|
||||
average: 5
|
||||
],
|
||||
...
|
||||
],
|
||||
...
|
||||
]
|
||||
*/
|
||||
|
||||
private PLATFORM_addDataToBucket(attribute, value, dateType) {
|
||||
|
||||
def numberValue = value.toBigDecimal()
|
||||
|
||||
def attributeKey = "measure.${attribute}"
|
||||
def attributeBucket = state[attributeKey] ?: [:]
|
||||
|
||||
def dateTypeBucket = attributeBucket[dateType] ?: []
|
||||
|
||||
def now = new Date()
|
||||
def itemDateString = now.format("PLATFORM_get${dateType}Format"())
|
||||
def item = dateTypeBucket.find { it.dateString == itemDateString }
|
||||
|
||||
if (!item) {
|
||||
// no entry for this data point yet, fill with initial values
|
||||
item = [:]
|
||||
item.average = numberValue
|
||||
item.runningSum = numberValue
|
||||
item.runningCount = 1
|
||||
item.min = numberValue
|
||||
item.max = numberValue
|
||||
item.unixTime = now.getTime()
|
||||
item.dateString = itemDateString
|
||||
|
||||
// add the new data point
|
||||
dateTypeBucket << item
|
||||
|
||||
// clear out old data points
|
||||
def old = PLATFORM_getOldDateString(dateType)
|
||||
if (old) { // annual data never gets cleared
|
||||
dateTypeBucket.findAll { it.unixTime < old }.each { dateTypeBucket.remove(it) }
|
||||
}
|
||||
|
||||
// limit the size of the bucket. Live data can stack up fast
|
||||
def sizeLimit = 25
|
||||
if (dateTypeBucket.size() > sizeLimit) {
|
||||
dateTypeBucket = dateTypeBucket[-sizeLimit..-1]
|
||||
}
|
||||
|
||||
} else {
|
||||
//re-calculate average/min/max for this bucket
|
||||
item.runningSum = (item.runningSum.toBigDecimal()) + numberValue
|
||||
item.runningCount = item.runningCount.toInteger() + 1
|
||||
item.average = item.runningSum.toBigDecimal() / item.runningCount.toInteger()
|
||||
|
||||
if (item.min == null) {
|
||||
item.min = numberValue
|
||||
} else if (numberValue < item.min.toBigDecimal()) {
|
||||
item.min = numberValue
|
||||
}
|
||||
if (item.max == null) {
|
||||
item.max = numberValue
|
||||
} else if (numberValue > item.max.toBigDecimal()) {
|
||||
item.max = numberValue
|
||||
}
|
||||
}
|
||||
|
||||
attributeBucket[dateType] = dateTypeBucket
|
||||
state[attributeKey] = attributeBucket
|
||||
}
|
||||
|
||||
private PLATFORM_getOldDateString(dateType) {
|
||||
def now = new Date()
|
||||
def date
|
||||
switch (dateType) {
|
||||
case "Live":
|
||||
date = now.getTime() - 60 * 60 * 1000 // 1h * 60m * 60s * 1000ms // 1 hour
|
||||
break
|
||||
case "Hourly":
|
||||
date = (now - 1).getTime()
|
||||
break
|
||||
case "Daily":
|
||||
date = (now - 10).getTime()
|
||||
break
|
||||
case "Monthly":
|
||||
date = (now - 30).getTime()
|
||||
break
|
||||
case "Annually":
|
||||
break
|
||||
}
|
||||
date
|
||||
}
|
||||
|
||||
private PLATFORM_getLiveFormat() { "HH:mm:ss" }
|
||||
|
||||
private PLATFORM_getHourlyFormat() { "yyyy-MM-dd'T'HH" }
|
||||
|
||||
private PLATFORM_getDailyFormat() { "yyyy-MM-dd" }
|
||||
|
||||
private PLATFORM_getMonthlyFormat() { "yyyy-MM" }
|
||||
|
||||
private PLATFORM_getAnnuallyFormat() { "yyyy" }
|
||||
|
||||
// ========================================================
|
||||
// PLATFORM GRAPH DATA MIGRATION
|
||||
// ========================================================
|
||||
|
||||
private PLATFORM_migrateGraphDataIfNeeded() {
|
||||
if (!state.hasMigratedOldGraphData) {
|
||||
def acceptableKeys = PLATFORM_graphPrecisionOptions()
|
||||
def needsMigration = state.findAll { k, v -> v.keySet().findAll { !acceptableKeys.contains(it) } }.keySet()
|
||||
needsMigration.each { PLATFORM_migrateGraphData(it) }
|
||||
state.hasMigratedOldGraphData = true
|
||||
}
|
||||
}
|
||||
|
||||
private PLATFORM_migrateGraphData(attribute) {
|
||||
|
||||
log.trace "about to migrate ${attribute}"
|
||||
|
||||
def attributeBucket = state[attribute] ?: [:]
|
||||
def migratedAttributeBucket = [:]
|
||||
|
||||
attributeBucket.findAll { k, v -> !PLATFORM_graphPrecisionOptions().contains(k) }.each { oldDateString, oldItem ->
|
||||
|
||||
def dateType = oldDateString.contains('T') ? "Hourly" : PLATFORM_graphPrecisionOptions().find {
|
||||
"PLATFORM_get${it}Format"().size() == oldDateString.size()
|
||||
}
|
||||
|
||||
def dateTypeFormat = "PLATFORM_get${dateType}Format"()
|
||||
|
||||
def newBucket = attributeBucket[dateType] ?: []
|
||||
/*
|
||||
def existingNewItem = newBucket.find { it.dateString == oldDateString }
|
||||
if (existingNewItem) {
|
||||
newBucket.remove(existingNewItem)
|
||||
}
|
||||
*/
|
||||
|
||||
def newItem = [
|
||||
min : oldItem.min,
|
||||
max : oldItem.max,
|
||||
average : oldItem.average,
|
||||
runningSum : oldItem.runningSum,
|
||||
runningCount: oldItem.runningCount,
|
||||
dateString : oldDateString,
|
||||
unixTime : new Date().parse(dateTypeFormat, oldDateString).getTime()
|
||||
]
|
||||
|
||||
newBucket << newItem
|
||||
migratedAttributeBucket[dateType] = newBucket
|
||||
}
|
||||
|
||||
state[attribute] = migratedAttributeBucket
|
||||
renderGraph(attribute: params.attribute, xValues: xValues, yValues: yValues, focus: "Total", label: "Watts")
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ metadata {
|
||||
capability "Battery"
|
||||
|
||||
attribute "tamper", "enum", ["detected", "clear"]
|
||||
attribute "batteryStatus", "string"
|
||||
attribute "powerSupply", "enum", ["USB Cable", "Battery"]
|
||||
|
||||
fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A", outClusters: "0x5A"
|
||||
}
|
||||
@@ -63,6 +65,19 @@ metadata {
|
||||
status "wake up" : "command: 8407, payload: "
|
||||
}
|
||||
|
||||
preferences {
|
||||
input description: "Please consult AEOTEC MULTISENSOR 6 operating manual for advanced setting options. You can skip this configuration to use default settings",
|
||||
title: "Advanced Configuration", displayDuringSetup: true, type: "paragraph", element: "paragraph"
|
||||
|
||||
input "motionDelayTime", "enum", title: "Motion Sensor Delay Time",
|
||||
options: ["20 seconds", "40 seconds", "1 minute", "2 minutes", "3 minutes", "4 minutes"], defaultValue: "${motionDelayTime}", displayDuringSetup: true
|
||||
|
||||
input "motionSensitivity", "enum", title: "Motion Sensor Sensitivity", options: ["normal","maximum","minimum"], defaultValue: "${motionSensitivity}", displayDuringSetup: true
|
||||
|
||||
input "reportInterval", "enum", title: "Sensors Report Interval",
|
||||
options: ["8 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${reportInterval}", displayDuringSetup: true
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"motion", type: "generic", width: 6, height: 4){
|
||||
tileAttribute ("device.motion", key: "PRIMARY_CONTROL") {
|
||||
@@ -85,53 +100,78 @@ metadata {
|
||||
valueTile("humidity", "device.humidity", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "humidity", label:'${currentValue}% humidity', unit:""
|
||||
}
|
||||
|
||||
valueTile("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "luminosity", label:'${currentValue} ${unit}', unit:"lux"
|
||||
state "illuminance", label:'${currentValue} ${unit}', unit:"lux"
|
||||
}
|
||||
|
||||
valueTile("ultravioletIndex", "device.ultravioletIndex", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "ultravioletIndex", label:'${currentValue} UV index', unit:""
|
||||
}
|
||||
|
||||
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
|
||||
main(["motion", "temperature", "humidity", "illuminance"])
|
||||
details(["motion", "temperature", "humidity", "illuminance", "battery"])
|
||||
valueTile("batteryStatus", "device.batteryStatus", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "batteryStatus", label:'${currentValue}', unit:""
|
||||
}
|
||||
|
||||
valueTile("powerSupply", "device.powerSupply", height: 2, width: 2, decoration: "flat") {
|
||||
state "powerSupply", label:'${currentValue} powered', backgroundColor:"#ffffff"
|
||||
}
|
||||
|
||||
main(["motion", "temperature", "humidity", "illuminance", "ultravioletIndex"])
|
||||
details(["motion", "temperature", "humidity", "illuminance", "ultravioletIndex", "batteryStatus"])
|
||||
}
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
if (state.sec && !isConfigured()) {
|
||||
// in case we miss the SCSR
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
log.debug "${device.displayName} is now ${device.latestValue("powerSupply")}"
|
||||
|
||||
if (device.latestValue("powerSupply") == "USB Cable") { //case1: USB powered
|
||||
response(configure())
|
||||
} else if (device.latestValue("powerSupply") == "Battery") { //case2: battery powered
|
||||
// setConfigured("false") is used by WakeUpNotification
|
||||
setConfigured("false") //wait until the next time device wakeup to send configure command after user change preference
|
||||
} else { //case3: power source is not identified, ask user to properly pair the sensor again
|
||||
log.warn "power source is not identified, check it sensor is powered by USB, if so > configure()"
|
||||
def request = []
|
||||
request << zwave.configurationV1.configurationGet(parameterNumber: 101)
|
||||
response(commands(request))
|
||||
}
|
||||
}
|
||||
|
||||
def parse(String description)
|
||||
{
|
||||
def parse(String description) {
|
||||
log.debug "parse() >> description: $description"
|
||||
def result = null
|
||||
if (description.startsWith("Err 106")) {
|
||||
state.sec = 0
|
||||
log.debug "parse() >> Err 106"
|
||||
result = createEvent( name: "secureInclusion", value: "failed", isStateChange: true,
|
||||
descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.")
|
||||
descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.")
|
||||
} else if (description != "updated") {
|
||||
log.debug "parse() >> zwave.parse(description)"
|
||||
def cmd = zwave.parse(description, [0x31: 5, 0x30: 2, 0x84: 1])
|
||||
if (cmd) {
|
||||
result = zwaveEvent(cmd)
|
||||
}
|
||||
}
|
||||
log.debug "Parsed '${description}' to ${result.inspect()}"
|
||||
log.debug "After zwaveEvent(cmd) >> Parsed '${description}' to ${result.inspect()}"
|
||||
return result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd)
|
||||
{
|
||||
//this notification will be sent only when device is battery powered
|
||||
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) {
|
||||
def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)]
|
||||
|
||||
def cmds = []
|
||||
if (!isConfigured()) {
|
||||
// we're still in the process of configuring a newly joined device
|
||||
log.debug("late configure")
|
||||
result += response(configure())
|
||||
result << response(configure())
|
||||
} else {
|
||||
result += response(zwave.wakeUpV1.wakeUpNoMoreInformation())
|
||||
log.debug("Device has been configured sending >> wakeUpNoMoreInformation()")
|
||||
cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format()
|
||||
result << response(cmds)
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -149,10 +189,29 @@ def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulat
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) {
|
||||
response(configure())
|
||||
log.info "Executing zwaveEvent 98 (SecurityV1): 03 (SecurityCommandsSupportedReport) with cmd: $cmd"
|
||||
state.sec = 1
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) {
|
||||
state.sec = 1
|
||||
log.info "Executing zwaveEvent 98 (SecurityV1): 07 (NetworkKeyVerify) with cmd: $cmd (node is securely included)"
|
||||
def result = [createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful", isStateChange: true)]
|
||||
result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
|
||||
log.info "Executing zwaveEvent 72 (ManufacturerSpecificV2) : 05 (ManufacturerSpecificReport) with cmd: $cmd"
|
||||
log.debug "manufacturerId: ${cmd.manufacturerId}"
|
||||
log.debug "manufacturerName: ${cmd.manufacturerName}"
|
||||
log.debug "productId: ${cmd.productId}"
|
||||
log.debug "productTypeId: ${cmd.productTypeId}"
|
||||
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
|
||||
updateDataValue("MSR", msr)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||
def result = []
|
||||
def map = [ name: "battery", unit: "%" ]
|
||||
if (cmd.batteryLevel == 0xFF) {
|
||||
map.value = 1
|
||||
@@ -162,11 +221,14 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||
map.value = cmd.batteryLevel
|
||||
}
|
||||
state.lastbatt = now()
|
||||
createEvent(map)
|
||||
result << createEvent(map)
|
||||
if (device.latestValue("powerSupply") != "USB Cable"){
|
||||
result << createEvent(name: "batteryStatus", value: "${map.value} % battery", displayed: false)
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd)
|
||||
{
|
||||
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd){
|
||||
def map = [:]
|
||||
switch (cmd.sensorType) {
|
||||
case 1:
|
||||
@@ -208,7 +270,6 @@ def motionEvent(value) {
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) {
|
||||
setConfigured()
|
||||
motionEvent(cmd.sensorValue)
|
||||
}
|
||||
|
||||
@@ -225,47 +286,112 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm
|
||||
result << createEvent(name: "tamper", value: "clear", displayed: false)
|
||||
break
|
||||
case 3:
|
||||
result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName was moved")
|
||||
result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName was tampered")
|
||||
break
|
||||
case 7:
|
||||
result << motionEvent(1)
|
||||
break
|
||||
}
|
||||
} else {
|
||||
log.warn "Need to handle this cmd.notificationType: ${cmd.notificationType}"
|
||||
result << createEvent(descriptionText: cmd.toString(), isStateChange: false)
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) {
|
||||
log.debug "ConfigurationReport: $cmd"
|
||||
def result = []
|
||||
def value
|
||||
if (cmd.parameterNumber == 9 && cmd.configurationValue[0] == 0) {
|
||||
value = "USB Cable"
|
||||
if (!isConfigured()) {
|
||||
log.debug("ConfigurationReport: configuring device")
|
||||
result << response(configure())
|
||||
}
|
||||
result << createEvent(name: "batteryStatus", value: value, displayed: false)
|
||||
result << createEvent(name: "powerSupply", value: value, displayed: false)
|
||||
}else if (cmd.parameterNumber == 9 && cmd.configurationValue[0] == 1) {
|
||||
value = "Battery"
|
||||
result << createEvent(name: "powerSupply", value: value, displayed: false)
|
||||
} else if (cmd.parameterNumber == 101){
|
||||
result << response(configure())
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
log.debug "General zwaveEvent cmd: ${cmd}"
|
||||
createEvent(descriptionText: cmd.toString(), isStateChange: false)
|
||||
}
|
||||
|
||||
def configure() {
|
||||
// This sensor joins as a secure device if you double-click the button to include it
|
||||
if (device.device.rawDescription =~ /98/ && !state.sec) {
|
||||
log.debug "Multi 6 not sending configure until secure"
|
||||
return []
|
||||
}
|
||||
log.debug "Multi 6 configure()"
|
||||
def request = [
|
||||
// send no-motion report 20 seconds after motion stops
|
||||
zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: 20),
|
||||
log.debug "${device.displayName} is configuring its settings"
|
||||
def request = []
|
||||
|
||||
// report every 8 minutes (threshold reports don't work on battery power)
|
||||
zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 8*60),
|
||||
//1. set association groups for hub
|
||||
request << zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)
|
||||
|
||||
// report automatically on threshold change
|
||||
zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 1),
|
||||
request << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId)
|
||||
|
||||
//2. automatic report flags
|
||||
// param 101 -103 [4 bytes] 128: light sensor, 64 humidity, 32 temperature sensor, 2 ultraviolet sensor, 1 battery sensor -> send command 227 to get all reports
|
||||
request << zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 226) //association group 1
|
||||
|
||||
request << zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 1) //association group 2
|
||||
|
||||
//3. no-motion report x seconds after motion stops (default 20 secs)
|
||||
request << zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: timeOptionValueMap[motionDelayTime] ?: 20)
|
||||
|
||||
//4. motionSensitivity 3 levels: 64-normal (default), 127-maximum, 0-minimum
|
||||
request << zwave.configurationV1.configurationSet(parameterNumber: 6, size: 1,
|
||||
scaledConfigurationValue:
|
||||
motionSensitivity == "normal" ? 64 :
|
||||
motionSensitivity == "maximum" ? 127 :
|
||||
motionSensitivity == "minimum" ? 0 : 64)
|
||||
|
||||
//5. report every x minutes (threshold reports don't work on battery power, default 8 mins)
|
||||
request << zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: timeOptionValueMap[reportInterval] ?: 8*60) //association group 1
|
||||
|
||||
request << zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 6*60*60) //association group 2
|
||||
|
||||
//6. report automatically on threshold change
|
||||
request << zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 1)
|
||||
|
||||
//7. query sensor data
|
||||
request << zwave.batteryV1.batteryGet()
|
||||
request << zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0x0C) //motion
|
||||
request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01) //temperature
|
||||
request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x03) //illuminance
|
||||
request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x05) //humidity
|
||||
request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x1B) //ultravioletIndex
|
||||
|
||||
setConfigured("true")
|
||||
|
||||
zwave.batteryV1.batteryGet(),
|
||||
zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0x0C),
|
||||
]
|
||||
commands(request) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]
|
||||
}
|
||||
|
||||
private setConfigured() {
|
||||
updateDataValue("configured", "true")
|
||||
private def getTimeOptionValueMap() { [
|
||||
"20 seconds" : 20,
|
||||
"40 seconds" : 40,
|
||||
"1 minute" : 60,
|
||||
"2 minutes" : 2*60,
|
||||
"3 minutes" : 3*60,
|
||||
"4 minutes" : 4*60,
|
||||
"5 minutes" : 5*60,
|
||||
"8 minutes" : 8*60,
|
||||
"15 minutes" : 15*60,
|
||||
"30 minutes" : 30*60,
|
||||
"1 hours" : 1*60*60,
|
||||
"6 hours" : 6*60*60,
|
||||
"12 hours" : 12*60*60,
|
||||
"18 hours" : 6*60*60,
|
||||
"24 hours" : 24*60*60,
|
||||
]}
|
||||
|
||||
private setConfigured(configure) {
|
||||
updateDataValue("configured", configure)
|
||||
}
|
||||
|
||||
private isConfigured() {
|
||||
@@ -281,5 +407,6 @@ private command(physicalgraph.zwave.Command cmd) {
|
||||
}
|
||||
|
||||
private commands(commands, delay=200) {
|
||||
log.info "sending commands: ${commands}"
|
||||
delayBetween(commands.collect{ command(it) }, delay)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 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: "Arrival Sensor HA", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Tone"
|
||||
capability "Actuator"
|
||||
capability "Presence Sensor"
|
||||
capability "Sensor"
|
||||
capability "Battery"
|
||||
capability "Configuration"
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,000F,0020", outClusters: "0003,0019",
|
||||
manufacturer: "SmartThings", model: "tagv4", deviceJoinName: "Arrival Sensor"
|
||||
}
|
||||
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival2.png"
|
||||
])
|
||||
}
|
||||
section {
|
||||
input "checkInterval", "enum", title: "Presence timeout (minutes)",
|
||||
defaultValue:"2", options: ["2", "3", "5"], displayDuringSetup: false
|
||||
}
|
||||
}
|
||||
|
||||
tiles {
|
||||
standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) {
|
||||
state "present", labelIcon:"st.presence.tile.present", backgroundColor:"#53a7c0"
|
||||
state "not present", labelIcon:"st.presence.tile.not-present", backgroundColor:"#ffffff"
|
||||
}
|
||||
standardTile("beep", "device.beep", decoration: "flat") {
|
||||
state "beep", label:'', action:"tone.beep", icon:"st.secondary.beep", backgroundColor:"#ffffff"
|
||||
}
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
|
||||
main "presence"
|
||||
details(["presence", "beep", "battery"])
|
||||
}
|
||||
}
|
||||
|
||||
def updated() {
|
||||
startTimer()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
def cmds = zigbee.configureReporting(0x0001, 0x0020, 0x20, 20, 20, 0x01)
|
||||
log.debug "configure -- cmds: ${cmds}"
|
||||
return cmds
|
||||
}
|
||||
|
||||
def beep() {
|
||||
log.debug "Sending Identify command to beep the sensor for 5 seconds"
|
||||
return zigbee.command(0x0003, 0x00, "0500")
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
state.lastCheckin = now()
|
||||
handlePresenceEvent(true)
|
||||
|
||||
if (description?.startsWith('read attr -')) {
|
||||
handleReportAttributeMessage(description)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
private handleReportAttributeMessage(String description) {
|
||||
def descMap = zigbee.parseDescriptionAsMap(description)
|
||||
|
||||
if (descMap.clusterInt == 0x0001 && descMap.attrInt == 0x0020) {
|
||||
handleBatteryEvent(Integer.parseInt(descMap.value, 16))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create battery event from reported battery voltage.
|
||||
*
|
||||
* @param volts Battery voltage in .1V increments
|
||||
*/
|
||||
private handleBatteryEvent(volts) {
|
||||
if (volts == 0 || volts == 255) {
|
||||
log.debug "Ignoring invalid value for voltage (${volts/10}V)"
|
||||
}
|
||||
else {
|
||||
def batteryMap = [28:100, 27:100, 26:100, 25:90, 24:90, 23:70,
|
||||
22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
|
||||
def minVolts = 15
|
||||
def maxVolts = 28
|
||||
|
||||
if (volts < minVolts)
|
||||
volts = minVolts
|
||||
else if (volts > maxVolts)
|
||||
volts = maxVolts
|
||||
def pct = batteryMap[volts]
|
||||
if (pct != null) {
|
||||
def linkText = getLinkText(device)
|
||||
def eventMap = [
|
||||
name: 'battery',
|
||||
value: pct,
|
||||
descriptionText: "${linkText} battery was ${pct}%"
|
||||
]
|
||||
log.debug "Creating battery event for voltage=${volts/10}V: ${eventMap}"
|
||||
sendEvent(eventMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handlePresenceEvent(present) {
|
||||
def wasPresent = device.currentState("presence")?.value == "present"
|
||||
if (!wasPresent && present) {
|
||||
log.debug "Sensor is present"
|
||||
startTimer()
|
||||
} else if (!present) {
|
||||
log.debug "Sensor is not present"
|
||||
stopTimer()
|
||||
}
|
||||
def linkText = getLinkText(device)
|
||||
def eventMap = [
|
||||
name: "presence",
|
||||
value: present ? "present" : "not present",
|
||||
linkText: linkText,
|
||||
descriptionText: "${linkText} has ${present ? 'arrived' : 'left'}",
|
||||
]
|
||||
log.debug "Creating presence event: ${eventMap}"
|
||||
sendEvent(eventMap)
|
||||
}
|
||||
|
||||
private startTimer() {
|
||||
log.debug "Scheduling periodic timer"
|
||||
schedule("0 * * * * ?", checkPresenceCallback)
|
||||
}
|
||||
|
||||
private stopTimer() {
|
||||
log.debug "Stopping periodic timer"
|
||||
unschedule()
|
||||
}
|
||||
|
||||
def checkPresenceCallback() {
|
||||
def timeSinceLastCheckin = (now() - state.lastCheckin) / 1000
|
||||
def theCheckInterval = (checkInterval ? checkInterval as int : 2) * 60
|
||||
log.debug "Sensor checked in ${timeSinceLastCheckin} seconds ago"
|
||||
if (timeSinceLastCheckin >= theCheckInterval) {
|
||||
handlePresenceEvent(false)
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "SmartSense Presence", namespace: "smartthings", author: "SmartThings") {
|
||||
definition (name: "Arrival Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Tone"
|
||||
capability "Actuator"
|
||||
capability "Signal Strength"
|
||||
@@ -34,8 +34,8 @@ metadata {
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival2.png"
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival1.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Arrival/Arrival2.jpg"
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ metadata {
|
||||
tiles {
|
||||
standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) {
|
||||
state "present", labelIcon:"st.presence.tile.present", backgroundColor:"#53a7c0"
|
||||
state "not present", labelIcon:"st.presence.tile.not-present", backgroundColor:"#ffffff"
|
||||
state "not present", labelIcon:"st.presence.tile.not-present", backgroundColor:"#ebeef2"
|
||||
}
|
||||
standardTile("beep", "device.beep", decoration: "flat") {
|
||||
state "beep", label:'', action:"tone.beep", icon:"st.secondary.beep", backgroundColor:"#ffffff"
|
||||
@@ -0,0 +1,989 @@
|
||||
/**
|
||||
* Bose SoundTouch
|
||||
*
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
// Needed to be able to serialize the XmlSlurper data back to XML
|
||||
import groovy.xml.XmlUtil
|
||||
|
||||
// for the UI
|
||||
metadata {
|
||||
definition (name: "Bose SoundTouch", namespace: "smartthings", author: "SmartThings") {
|
||||
/**
|
||||
* List our capabilties. Doing so adds predefined command(s) which
|
||||
* belong to the capability.
|
||||
*/
|
||||
capability "Switch"
|
||||
capability "Refresh"
|
||||
capability "Music Player"
|
||||
capability "Polling"
|
||||
|
||||
/**
|
||||
* Define all commands, ie, if you have a custom action not
|
||||
* covered by a capability, you NEED to define it here or
|
||||
* the call will not be made.
|
||||
*
|
||||
* To call a capability function, just prefix it with the name
|
||||
* of the capability, for example, refresh would be "refresh.refresh"
|
||||
*/
|
||||
command "preset1"
|
||||
command "preset2"
|
||||
command "preset3"
|
||||
command "preset4"
|
||||
command "preset5"
|
||||
command "preset6"
|
||||
command "aux"
|
||||
|
||||
command "everywhereJoin"
|
||||
command "everywhereLeave"
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the various tiles and the states that they can be in.
|
||||
* The 2nd parameter defines an event which the tile listens to,
|
||||
* if received, it tries to map it to a state.
|
||||
*
|
||||
* You can also use ${currentValue} for the value of the event
|
||||
* or ${name} for the name of the event. Just make SURE to use
|
||||
* single quotes, otherwise it will only be interpreted at time of
|
||||
* launch, instead of every time the event triggers.
|
||||
*/
|
||||
valueTile("nowplaying", "device.nowplaying", width: 2, height: 1, decoration:"flat") {
|
||||
state "nowplaying", label:'${currentValue}', action:"refresh.refresh"
|
||||
}
|
||||
|
||||
standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) {
|
||||
state "off", label: '${name}', action: "switch.on", icon: "st.Electronics.electronics16", backgroundColor: "#ffffff"
|
||||
state "on", label: '${name}', action: "switch.off", icon: "st.Electronics.electronics16", backgroundColor: "#79b821"
|
||||
}
|
||||
valueTile("1", "device.station1", decoration: "flat", canChangeIcon: false) {
|
||||
state "station1", label:'${currentValue}', action:"preset1"
|
||||
}
|
||||
valueTile("2", "device.station2", decoration: "flat", canChangeIcon: false) {
|
||||
state "station2", label:'${currentValue}', action:"preset2"
|
||||
}
|
||||
valueTile("3", "device.station3", decoration: "flat", canChangeIcon: false) {
|
||||
state "station3", label:'${currentValue}', action:"preset3"
|
||||
}
|
||||
valueTile("4", "device.station4", decoration: "flat", canChangeIcon: false) {
|
||||
state "station4", label:'${currentValue}', action:"preset4"
|
||||
}
|
||||
valueTile("5", "device.station5", decoration: "flat", canChangeIcon: false) {
|
||||
state "station5", label:'${currentValue}', action:"preset5"
|
||||
}
|
||||
valueTile("6", "device.station6", decoration: "flat", canChangeIcon: false) {
|
||||
state "station6", label:'${currentValue}', action:"preset6"
|
||||
}
|
||||
valueTile("aux", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||
state "default", label:'Auxillary\nInput', action:"aux"
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.nowplaying", decoration: "flat", canChangeIcon: false) {
|
||||
state "default", label:'', action:"refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
controlTile("volume", "device.volume", "slider", height:1, width:3, range:"(0..100)") {
|
||||
state "volume", action:"music Player.setLevel"
|
||||
}
|
||||
|
||||
standardTile("playpause", "device.playpause", decoration: "flat") {
|
||||
state "pause", label:'', icon:'st.sonos.play-btn', action:'music Player.play'
|
||||
state "play", label:'', icon:'st.sonos.pause-btn', action:'music Player.pause'
|
||||
}
|
||||
|
||||
standardTile("prev", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||
state "default", label:'', action:"music Player.previousTrack", icon:"st.sonos.previous-btn"
|
||||
}
|
||||
standardTile("next", "device.switch", decoration: "flat", canChangeIcon: false) {
|
||||
state "default", label:'', action:"music Player.nextTrack", icon:"st.sonos.next-btn"
|
||||
}
|
||||
|
||||
valueTile("everywhere", "device.everywhere", width:2, height:1, decoration:"flat") {
|
||||
state "join", label:"Join\nEverywhere", action:"everywhereJoin"
|
||||
state "leave", label:"Leave\nEverywhere", action:"everywhereLeave"
|
||||
// Final state is used if the device is in a state where joining is not possible
|
||||
state "unavailable", label:"Not Available"
|
||||
}
|
||||
|
||||
// Defines which tile to show in the overview
|
||||
main "switch"
|
||||
|
||||
// Defines which tile(s) to show when user opens the detailed view
|
||||
details ([
|
||||
"nowplaying", "refresh", // Row 1 (112)
|
||||
"prev", "playpause", "next", // Row 2 (123)
|
||||
"volume", // Row 3 (111)
|
||||
"1", "2", "3", // Row 4 (123)
|
||||
"4", "5", "6", // Row 5 (123)
|
||||
"aux", "everywhere"]) // Row 6 (122)
|
||||
}
|
||||
|
||||
/**************************************************************************
|
||||
* The following section simply maps the actions as defined in
|
||||
* the metadata into onAction() calls.
|
||||
*
|
||||
* This is preferred since some actions can be dealt with more
|
||||
* efficiently this way. Also keeps all user interaction code in
|
||||
* one place.
|
||||
*
|
||||
*/
|
||||
def off() { onAction("off") }
|
||||
def on() { onAction("on") }
|
||||
def volup() { onAction("volup") }
|
||||
def voldown() { onAction("voldown") }
|
||||
def preset1() { onAction("1") }
|
||||
def preset2() { onAction("2") }
|
||||
def preset3() { onAction("3") }
|
||||
def preset4() { onAction("4") }
|
||||
def preset5() { onAction("5") }
|
||||
def preset6() { onAction("6") }
|
||||
def aux() { onAction("aux") }
|
||||
def refresh() { onAction("refresh") }
|
||||
def setLevel(level) { onAction("volume", level) }
|
||||
def play() { onAction("play") }
|
||||
def pause() { onAction("pause") }
|
||||
def mute() { onAction("mute") }
|
||||
def unmute() { onAction("unmute") }
|
||||
def previousTrack() { onAction("previous") }
|
||||
def nextTrack() { onAction("next") }
|
||||
def everywhereJoin() { onAction("ejoin") }
|
||||
def everywhereLeave() { onAction("eleave") }
|
||||
/**************************************************************************/
|
||||
|
||||
/**
|
||||
* Main point of interaction with things.
|
||||
* This function is called by SmartThings Cloud with the resulting data from
|
||||
* any action (see HubAction()).
|
||||
*
|
||||
* Conversely, to execute any actions, you need to return them as a single
|
||||
* item or a list (flattened).
|
||||
*
|
||||
* @param data Data provided by the cloud
|
||||
* @return an action or a list() of actions. Can also return null if no further
|
||||
* action is desired at this point.
|
||||
*/
|
||||
def parse(String event) {
|
||||
def data = parseLanMessage(event)
|
||||
def actions = []
|
||||
|
||||
// List of permanent root node handlers
|
||||
def handlers = [
|
||||
"nowPlaying" : "boseParseNowPlaying",
|
||||
"volume" : "boseParseVolume",
|
||||
"presets" : "boseParsePresets",
|
||||
"zone" : "boseParseEverywhere",
|
||||
]
|
||||
|
||||
// No need to deal with non-XML data
|
||||
if (!data.headers || !data.headers?."content-type".contains("xml"))
|
||||
return null
|
||||
|
||||
// Move any pending callbacks into ready state
|
||||
prepareCallbacks()
|
||||
|
||||
def xml = new XmlSlurper().parseText(data.body)
|
||||
// Let each parser take a stab at it
|
||||
handlers.each { node,func ->
|
||||
if (xml.name() == node)
|
||||
actions << "$func"(xml)
|
||||
}
|
||||
// If we have callbacks waiting for this...
|
||||
actions << processCallbacks(xml)
|
||||
|
||||
// Be nice and helpful
|
||||
if (actions.size() == 0) {
|
||||
log.warn "parse(): Unhandled data = " + lan
|
||||
return null
|
||||
}
|
||||
|
||||
// Issue new actions
|
||||
return actions.flatten()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the devicetype is first installed.
|
||||
*
|
||||
* @return action(s) to take or null
|
||||
*/
|
||||
def installed() {
|
||||
onAction("refresh")
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for dealing with user input and taking the
|
||||
* appropiate action.
|
||||
*
|
||||
* @param user The user interaction
|
||||
* @param data Additional data (optional)
|
||||
* @return action(s) to take (or null if none)
|
||||
*/
|
||||
def onAction(String user, data=null) {
|
||||
log.info "onAction(${user})"
|
||||
|
||||
// Keep IP address current (since device may have changed)
|
||||
state.address = parent.resolveDNI2Address(device.deviceNetworkId)
|
||||
|
||||
// Process action
|
||||
def actions = null
|
||||
switch (user) {
|
||||
case "on":
|
||||
actions = boseSetPowerState(true)
|
||||
break
|
||||
case "off":
|
||||
boseSetNowPlaying(null, "STANDBY")
|
||||
actions = boseSetPowerState(false)
|
||||
break
|
||||
case "volume":
|
||||
actions = boseSetVolume(data)
|
||||
break
|
||||
case "aux":
|
||||
boseSetNowPlaying(null, "AUX")
|
||||
boseZoneReset()
|
||||
sendEvent(name:"everywhere", value:"unavailable")
|
||||
case "1":
|
||||
case "2":
|
||||
case "3":
|
||||
case "4":
|
||||
case "5":
|
||||
case "6":
|
||||
actions = boseSetInput(user)
|
||||
break
|
||||
case "refresh":
|
||||
boseSetNowPlaying(null, "REFRESH")
|
||||
actions = [boseRefreshNowPlaying(), boseGetPresets(), boseGetVolume(), boseGetEverywhereState()]
|
||||
break
|
||||
case "play":
|
||||
actions = [boseSetPlayMode(true), boseRefreshNowPlaying()]
|
||||
break
|
||||
case "pause":
|
||||
actions = [boseSetPlayMode(false), boseRefreshNowPlaying()]
|
||||
break
|
||||
case "previous":
|
||||
actions = [boseChangeTrack(-1), boseRefreshNowPlaying()]
|
||||
break
|
||||
case "next":
|
||||
actions = [boseChangeTrack(1), boseRefreshNowPlaying()]
|
||||
break
|
||||
case "mute":
|
||||
actions = boseSetMute(true)
|
||||
break
|
||||
case "unmute":
|
||||
actions = boseSetMute(false)
|
||||
break
|
||||
case "ejoin":
|
||||
actions = boseZoneJoin()
|
||||
break
|
||||
case "eleave":
|
||||
actions = boseZoneLeave()
|
||||
break
|
||||
default:
|
||||
log.error "Unhandled action: " + user
|
||||
}
|
||||
|
||||
// Make sure we don't have nested lists
|
||||
if (actions instanceof List)
|
||||
return actions.flatten()
|
||||
return actions
|
||||
}
|
||||
|
||||
/**
|
||||
* Called every so often (every 5 minutes actually) to refresh the
|
||||
* tiles so the user gets the correct information.
|
||||
*/
|
||||
def poll() {
|
||||
return boseRefreshNowPlaying()
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins this speaker into the everywhere zone
|
||||
*/
|
||||
def boseZoneJoin() {
|
||||
def results = []
|
||||
def posts = parent.boseZoneJoin(this)
|
||||
|
||||
for (post in posts) {
|
||||
if (post['endpoint'])
|
||||
results << bosePOST(post['endpoint'], post['body'], post['host'])
|
||||
}
|
||||
sendEvent(name:"everywhere", value:"leave")
|
||||
results << boseRefreshNowPlaying()
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes this speaker from the everywhere zone
|
||||
*/
|
||||
def boseZoneLeave() {
|
||||
def results = []
|
||||
def posts = parent.boseZoneLeave(this)
|
||||
|
||||
for (post in posts) {
|
||||
if (post['endpoint'])
|
||||
results << bosePOST(post['endpoint'], post['body'], post['host'])
|
||||
}
|
||||
sendEvent(name:"everywhere", value:"join")
|
||||
results << boseRefreshNowPlaying()
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes this speaker and any children WITHOUT
|
||||
* signaling the speakers themselves. This is needed
|
||||
* in certain cases where we know the user action will
|
||||
* cause the zone to collapse (for example, AUX)
|
||||
*/
|
||||
def boseZoneReset() {
|
||||
parent.boseZoneReset()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles <nowPlaying></nowPlaying> information and can also
|
||||
* perform addtional actions if there is a pending command
|
||||
* stored in the state variable. For example, the power is
|
||||
* handled this way.
|
||||
*
|
||||
* @param xmlData Data to parse
|
||||
* @return command
|
||||
*/
|
||||
def boseParseNowPlaying(xmlData) {
|
||||
def result = []
|
||||
|
||||
// Perform display update, allow it to add additional commands
|
||||
if (boseSetNowPlaying(xmlData)) {
|
||||
result << boseRefreshNowPlaying()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses volume data
|
||||
*
|
||||
* @param xmlData Data to parse
|
||||
* @return command
|
||||
*/
|
||||
def boseParseVolume(xmlData) {
|
||||
def result = []
|
||||
|
||||
sendEvent(name:"volume", value:xmlData.actualvolume.text())
|
||||
sendEvent(name:"mute", value:(Boolean.toBoolean(xmlData.muteenabled.text()) ? "unmuted" : "muted"))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the result of the boseGetEverywhereState() call
|
||||
*
|
||||
* @param xmlData
|
||||
*/
|
||||
def boseParseEverywhere(xmlData) {
|
||||
// No good way of detecting the correct state right now
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses presets and updates the buttons
|
||||
*
|
||||
* @param xmlData Data to parse
|
||||
* @return command
|
||||
*/
|
||||
def boseParsePresets(xmlData) {
|
||||
def result = []
|
||||
|
||||
state.preset = [:]
|
||||
|
||||
def missing = ["1", "2", "3", "4", "5", "6"]
|
||||
for (preset in xmlData.preset) {
|
||||
def id = preset.attributes()['id']
|
||||
def name = preset.ContentItem.itemName[0].text().replaceAll(~/ +/, "\n")
|
||||
if (name == "##TRANS_SONGS##")
|
||||
name = "Local\nPlaylist"
|
||||
sendEvent(name:"station${id}", value:name)
|
||||
missing = missing.findAll { it -> it != id }
|
||||
|
||||
// Store the presets into the state for recall later
|
||||
state.preset["$id"] = XmlUtil.serialize(preset.ContentItem)
|
||||
}
|
||||
|
||||
for (id in missing) {
|
||||
state.preset["$id"] = null
|
||||
sendEvent(name:"station${id}", value:"Preset $id\n\nNot set")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on <nowPlaying></nowPlaying>, updates the visual
|
||||
* representation of the speaker
|
||||
*
|
||||
* @param xmlData The nowPlaying info
|
||||
* @param override Provide the source type manually (optional)
|
||||
*
|
||||
* @return true if it would prefer a refresh soon
|
||||
*/
|
||||
def boseSetNowPlaying(xmlData, override=null) {
|
||||
def needrefresh = false
|
||||
def nowplaying = null
|
||||
|
||||
if (xmlData && xmlData.playStatus) {
|
||||
switch(xmlData.playStatus) {
|
||||
case "BUFFERING_STATE":
|
||||
nowplaying = "Please wait\nBuffering..."
|
||||
needrefresh = true
|
||||
break
|
||||
case "PLAY_STATE":
|
||||
sendEvent(name:"playpause", value:"play")
|
||||
break
|
||||
case "PAUSE_STATE":
|
||||
case "STOP_STATE":
|
||||
sendEvent(name:"playpause", value:"pause")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If the previous section didn't handle this, take another stab at it
|
||||
if (!nowplaying) {
|
||||
nowplaying = ""
|
||||
switch (override ? override : xmlData.attributes()['source']) {
|
||||
case "AUX":
|
||||
nowplaying = "Auxiliary Input"
|
||||
break
|
||||
case "AIRPLAY":
|
||||
nowplaying = "Air Play"
|
||||
break
|
||||
case "STANDBY":
|
||||
nowplaying = "Standby"
|
||||
break
|
||||
case "INTERNET_RADIO":
|
||||
nowplaying = "${xmlData.stationName.text()}\n\n${xmlData.description.text()}"
|
||||
break
|
||||
case "REFRESH":
|
||||
nowplaying = "Please wait"
|
||||
break
|
||||
case "SPOTIFY":
|
||||
case "DEEZER":
|
||||
case "PANDORA":
|
||||
case "IHEART":
|
||||
if (xmlData.ContentItem.itemName[0])
|
||||
nowplaying += "[${xmlData.ContentItem.itemName[0].text()}]\n\n"
|
||||
case "STORED_MUSIC":
|
||||
nowplaying += "${xmlData.track.text()}"
|
||||
if (xmlData.artist)
|
||||
nowplaying += "\nby\n${xmlData.artist.text()}"
|
||||
if (xmlData.album)
|
||||
nowplaying += "\n\n(${xmlData.album.text()})"
|
||||
break
|
||||
default:
|
||||
if (xmlData != null)
|
||||
nowplaying = "${xmlData.ContentItem.itemName[0].text()}"
|
||||
else
|
||||
nowplaying = "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Some last parsing which only deals with actual data from device
|
||||
if (xmlData) {
|
||||
if (xmlData.attributes()['source'] == "STANDBY") {
|
||||
log.trace "nowPlaying reports standby: " + XmlUtil.serialize(xmlData)
|
||||
sendEvent(name:"switch", value:"off")
|
||||
} else {
|
||||
sendEvent(name:"switch", value:"on")
|
||||
}
|
||||
boseSetPlayerAttributes(xmlData)
|
||||
}
|
||||
|
||||
// Do not allow a standby device or AUX to be master
|
||||
if (!parent.boseZoneHasMaster() && (override ? override : xmlData.attributes()['source']) == "STANDBY")
|
||||
sendEvent(name:"everywhere", value:"unavailable")
|
||||
else if ((override ? override : xmlData.attributes()['source']) == "AUX")
|
||||
sendEvent(name:"everywhere", value:"unavailable")
|
||||
else if (boseGetZone()) {
|
||||
log.info "We're in the zone: " + boseGetZone()
|
||||
sendEvent(name:"everywhere", value:"leave")
|
||||
} else
|
||||
sendEvent(name:"everywhere", value:"join")
|
||||
|
||||
sendEvent(name:"nowplaying", value:nowplaying)
|
||||
|
||||
return needrefresh
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the attributes exposed by the music Player capability
|
||||
*
|
||||
* @param xmlData The NowPlaying XML data
|
||||
*/
|
||||
def boseSetPlayerAttributes(xmlData) {
|
||||
// Refresh attributes
|
||||
def trackText = ""
|
||||
def trackDesc = ""
|
||||
def trackData = [:]
|
||||
|
||||
switch (xmlData.attributes()['source']) {
|
||||
case "STANDBY":
|
||||
trackData["station"] = trackText = trackDesc = "Standby"
|
||||
break
|
||||
case "AUX":
|
||||
trackData["station"] = trackText = trackDesc = "Auxiliary Input"
|
||||
break
|
||||
case "AIRPLAY":
|
||||
trackData["station"] = trackText = trackDesc = "Air Play"
|
||||
break
|
||||
case "SPOTIFY":
|
||||
case "DEEZER":
|
||||
case "PANDORA":
|
||||
case "IHEART":
|
||||
case "STORED_MUSIC":
|
||||
trackText = trackDesc = "${xmlData.track.text()}"
|
||||
trackData["name"] = xmlData.track.text()
|
||||
if (xmlData.artist) {
|
||||
trackText += " by ${xmlData.artist.text()}"
|
||||
trackDesc += " - ${xmlData.artist.text()}"
|
||||
trackData["artist"] = xmlData.artist.text()
|
||||
}
|
||||
if (xmlData.album) {
|
||||
trackText += " (${xmlData.album.text()})"
|
||||
trackData["album"] = xmlData.album.text()
|
||||
}
|
||||
break
|
||||
case "INTERNET_RADIO":
|
||||
trackDesc = xmlData.stationName.text()
|
||||
trackText = xmlData.stationName.text() + ": " + xmlData.description.text()
|
||||
trackData["station"] = xmlData.stationName.text()
|
||||
break
|
||||
default:
|
||||
trackText = trackDesc = xmlData.ContentItem.itemName[0].text()
|
||||
}
|
||||
|
||||
sendEvent(name:"trackDescription", value:trackDesc, descriptionText:trackText)
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the state of the "play everywhere" mode
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def boseGetEverywhereState() {
|
||||
return boseGET("/getZone")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a remote key event
|
||||
*
|
||||
* @param key The name of the key
|
||||
*
|
||||
* @return command
|
||||
*
|
||||
* @note It's VITAL that it's done as two requests, or it will ignore the
|
||||
* the second key info.
|
||||
*/
|
||||
def boseKeypress(key) {
|
||||
def press = "<key state=\"press\" sender=\"Gabbo\">${key}</key>"
|
||||
def release = "<key state=\"release\" sender=\"Gabbo\">${key}</key>"
|
||||
|
||||
return [bosePOST("/key", press), bosePOST("/key", release)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses or plays current preset
|
||||
*
|
||||
* @param play If true, plays, else it pauses (depending on preset, may stop)
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def boseSetPlayMode(boolean play) {
|
||||
log.trace "Sending " + (play ? "PLAY" : "PAUSE")
|
||||
return boseKeypress(play ? "PLAY" : "PAUSE")
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the volume in a deterministic way.
|
||||
*
|
||||
* @param New volume level, ranging from 0 to 100
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def boseSetVolume(int level) {
|
||||
def result = []
|
||||
int vol = Math.min(100, Math.max(level, 0))
|
||||
|
||||
sendEvent(name:"volume", value:"${vol}")
|
||||
|
||||
return [bosePOST("/volume", "<volume>${vol}</volume>"), boseGetVolume()]
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the mute state, unfortunately, for now, we need to query current
|
||||
* state before taking action (no discrete mute/unmute)
|
||||
*
|
||||
* @param mute If true, mutes the system
|
||||
* @return command
|
||||
*/
|
||||
def boseSetMute(boolean mute) {
|
||||
queueCallback('volume', 'cb_boseSetMute', mute ? 'MUTE' : 'UNMUTE')
|
||||
return boseGetVolume()
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for boseSetMute(), checks current state and changes it
|
||||
* if it doesn't match the requested state.
|
||||
*
|
||||
* @param xml The volume XML data
|
||||
* @param mute The new state of mute
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def cb_boseSetMute(xml, mute) {
|
||||
def result = []
|
||||
if ((xml.muteenabled.text() == 'false' && mute == 'MUTE') ||
|
||||
(xml.muteenabled.text() == 'true' && mute == 'UNMUTE'))
|
||||
{
|
||||
result << boseKeypress("MUTE")
|
||||
}
|
||||
log.trace("muteunmute: " + ((mute == "MUTE") ? "unmute" : "mute"))
|
||||
sendEvent(name:"muteunmute", value:((mute == "MUTE") ? "unmute" : "mute"))
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the state of the volume
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def boseGetVolume() {
|
||||
return boseGET("/volume")
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the track to either the previous or next
|
||||
*
|
||||
* @param direction > 0 = next track, < 0 = previous track, 0 = no action
|
||||
* @return command
|
||||
*/
|
||||
def boseChangeTrack(int direction) {
|
||||
if (direction < 0) {
|
||||
return boseKeypress("PREV_TRACK")
|
||||
} else if (direction > 0) {
|
||||
return boseKeypress("NEXT_TRACK")
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the input to preset 1-6 or AUX
|
||||
*
|
||||
* @param input The input (one of 1,2,3,4,5,6,aux)
|
||||
*
|
||||
* @return command
|
||||
*
|
||||
* @note If no presets have been loaded, it will first refresh the presets.
|
||||
*/
|
||||
def boseSetInput(input) {
|
||||
log.info "boseSetInput(${input})"
|
||||
def result = []
|
||||
|
||||
if (!state.preset) {
|
||||
result << boseGetPresets()
|
||||
queueCallback('presets', 'cb_boseSetInput', input)
|
||||
} else {
|
||||
result << cb_boseSetInput(null, input)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used by boseSetInput(), either called directly by
|
||||
* boseSetInput() if we already have presets, or called after
|
||||
* retreiving the presets for the first time.
|
||||
*
|
||||
* @param xml The presets XML data
|
||||
* @param input Desired input
|
||||
*
|
||||
* @return command
|
||||
*
|
||||
* @note Uses KEY commands for AUX, otherwise /select endpoint.
|
||||
* Reason for this is latency. Since keypresses are done
|
||||
* in pairs (press + release), you could accidentally change
|
||||
* the preset if there is a long delay between the two.
|
||||
*/
|
||||
def cb_boseSetInput(xml, input) {
|
||||
def result = []
|
||||
|
||||
if (input >= "1" && input <= "6" && state.preset["$input"])
|
||||
result << bosePOST("/select", state.preset["$input"])
|
||||
else if (input.toLowerCase() == "aux") {
|
||||
result << boseKeypress("AUX_INPUT")
|
||||
}
|
||||
|
||||
// Horrible workaround... but we need to delay
|
||||
// the update by at least a few seconds...
|
||||
result << boseRefreshNowPlaying(3000)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the power state of the bose unit
|
||||
*
|
||||
* @param device The device in-question
|
||||
* @param enable True to power on, false to power off
|
||||
*
|
||||
* @return command
|
||||
*
|
||||
* @note Will first query state before acting since there
|
||||
* is no discreete call.
|
||||
*/
|
||||
def boseSetPowerState(boolean enable) {
|
||||
log.info "boseSetPowerState(${enable})"
|
||||
queueCallback('nowPlaying', "cb_boseSetPowerState", enable ? "POWERON" : "POWEROFF")
|
||||
return boseRefreshNowPlaying()
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function used by boseSetPowerState(), is used
|
||||
* to handle the fact that we only have a toggle for power.
|
||||
*
|
||||
* @param xml The XML data from nowPlaying
|
||||
* @param state The requested state
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def cb_boseSetPowerState(xml, state) {
|
||||
def result = []
|
||||
if ( (xml.attributes()['source'] == "STANDBY" && state == "POWERON") ||
|
||||
(xml.attributes()['source'] != "STANDBY" && state == "POWEROFF") )
|
||||
{
|
||||
result << boseKeypress("POWER")
|
||||
if (state == "POWERON") {
|
||||
result << boseRefreshNowPlaying()
|
||||
queueCallback('nowPlaying', "cb_boseConfirmPowerOn", 5)
|
||||
}
|
||||
}
|
||||
return result.flatten()
|
||||
}
|
||||
|
||||
/**
|
||||
* We're sometimes too quick on the draw and get a refreshed nowPlaying
|
||||
* which shows standby (essentially, the device has yet to completely
|
||||
* transition to awake state), so we need to poll a few times extra
|
||||
* to make sure we get it right.
|
||||
*
|
||||
* @param xml The XML data from nowPlaying
|
||||
* @param tries A counter which will decrease, once it reaches zero,
|
||||
* we give up and assume that whatever we got was correct.
|
||||
* @return command
|
||||
*/
|
||||
def cb_boseConfirmPowerOn(xml, tries) {
|
||||
def result = []
|
||||
log.warn "boseConfirmPowerOn() attempt #" + tries
|
||||
if (xml.attributes()['source'] == "STANDBY" && tries > 0) {
|
||||
result << boseRefreshNowPlaying()
|
||||
queueCallback('nowPlaying', "cb_boseConfirmPowerOn", tries-1)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests an update on currently playing item(s)
|
||||
*
|
||||
* @param delay If set to non-zero, delays x ms before issuing
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def boseRefreshNowPlaying(delay=0) {
|
||||
if (delay > 0) {
|
||||
return ["delay ${delay}", boseGET("/now_playing")]
|
||||
}
|
||||
return boseGET("/now_playing")
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the list of presets
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def boseGetPresets() {
|
||||
return boseGET("/presets")
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function, makes GET requests to BOSE device
|
||||
*
|
||||
* @param path What endpoint
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def boseGET(String path) {
|
||||
new physicalgraph.device.HubAction([
|
||||
method: "GET",
|
||||
path: path,
|
||||
headers: [
|
||||
HOST: state.address + ":8090",
|
||||
]])
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function, makes a POST request to the BOSE device with
|
||||
* the provided data.
|
||||
*
|
||||
* @param path What endpoint
|
||||
* @param data What data
|
||||
* @param address Specific ip and port (optional)
|
||||
*
|
||||
* @return command
|
||||
*/
|
||||
def bosePOST(String path, String data, String address=null) {
|
||||
new physicalgraph.device.HubAction([
|
||||
method: "POST",
|
||||
path: path,
|
||||
body: data,
|
||||
headers: [
|
||||
HOST: address ?: (state.address + ":8090"),
|
||||
]])
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a callback function for when a specific XML root is received
|
||||
* Will execute on subsequent parse() call(s), never on the current
|
||||
* parse() call.
|
||||
*
|
||||
* @param root The root node that this callback should react to
|
||||
* @param func Name of the function
|
||||
* @param param Parameters for function (optional)
|
||||
*/
|
||||
def queueCallback(String root, String func, param=null) {
|
||||
if (!state.pending)
|
||||
state.pending = [:]
|
||||
if (!state.pending[root])
|
||||
state.pending[root] = []
|
||||
state.pending[root] << ["$func":"$param"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfers the pending callbacks into readiness state
|
||||
* so they can be executed by processCallbacks()
|
||||
*
|
||||
* This is needed to avoid reacting to queueCallbacks() within
|
||||
* the same loop.
|
||||
*/
|
||||
def prepareCallbacks() {
|
||||
if (!state.pending)
|
||||
return
|
||||
if (!state.ready)
|
||||
state.ready = [:]
|
||||
state.ready << state.pending
|
||||
state.pending = [:]
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes any ready callback for a specific root node
|
||||
* with associated parameter and then clears that entry.
|
||||
*
|
||||
* If a callback returns data, it's added to a list of
|
||||
* commands which is returned to the caller of this function
|
||||
*
|
||||
* Once a callback has been used, it's removed from the list
|
||||
* of queued callbacks (ie, it executes only once!)
|
||||
*
|
||||
* @param xml The XML data to be examined and delegated
|
||||
* @return list of commands
|
||||
*/
|
||||
def processCallbacks(xml) {
|
||||
def result = []
|
||||
|
||||
if (!state.ready)
|
||||
return result
|
||||
|
||||
if (state.ready[xml.name()]) {
|
||||
state.ready[xml.name()].each { callback ->
|
||||
callback.each { func, param ->
|
||||
if (func != "func") {
|
||||
if (param)
|
||||
result << "$func"(xml, param)
|
||||
else
|
||||
result << "$func"(xml)
|
||||
}
|
||||
}
|
||||
}
|
||||
state.ready.remove(xml.name())
|
||||
}
|
||||
return result.flatten()
|
||||
}
|
||||
|
||||
/**
|
||||
* State managament for the Play Everywhere zone.
|
||||
* This is typically called from the parent.
|
||||
*
|
||||
* A device is either:
|
||||
*
|
||||
* null = Not participating
|
||||
* server = running the show
|
||||
* client = under the control of the server
|
||||
*
|
||||
* @param newstate (see above for types)
|
||||
*/
|
||||
def boseSetZone(String newstate) {
|
||||
log.debug "boseSetZone($newstate)"
|
||||
state.zone = newstate
|
||||
|
||||
// Refresh our state
|
||||
if (newstate) {
|
||||
sendEvent(name:"everywhere", value:"leave")
|
||||
} else {
|
||||
sendEvent(name:"everywhere", value:"join")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the Everywhere zone, returns the current state
|
||||
* of zone membership (null, server, client)
|
||||
* This is typically called from the parent.
|
||||
*
|
||||
* @return state
|
||||
*/
|
||||
def boseGetZone() {
|
||||
return state.zone
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the DeviceID of this particular device.
|
||||
*
|
||||
* Needs to be done this way since DNI is not always
|
||||
* the same as DeviceID which is used internally by
|
||||
* BOSE.
|
||||
*
|
||||
* @param devID The DeviceID
|
||||
*/
|
||||
def boseSetDeviceID(String devID) {
|
||||
state.deviceID = devID
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the DeviceID for this device
|
||||
*
|
||||
* @return deviceID
|
||||
*/
|
||||
def boseGetDeviceID() {
|
||||
return state.deviceID
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the IP of this device
|
||||
*
|
||||
* @return IP address
|
||||
*/
|
||||
def getDeviceIP() {
|
||||
return parent.resolveDNI2Address(device.deviceNetworkId)
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
* Author: SmartThings
|
||||
* Date: 2013-12-04
|
||||
*/
|
||||
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
|
||||
metadata {
|
||||
definition (name: "CentraLite Dimmer", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Switch Level"
|
||||
@@ -25,7 +26,6 @@ metadata {
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B04,0B05", outClusters: "0019"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
|
||||
@@ -38,168 +38,63 @@ metadata {
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles {
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff"
|
||||
state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||
state "level", label: 'Level ${currentValue}%'
|
||||
}
|
||||
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "level", "levelSliderControl", "refresh"])
|
||||
}
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel"
|
||||
}
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
main "switch"
|
||||
details(["switch", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
|
||||
def parse(String description) {
|
||||
log.trace description
|
||||
if (description?.startsWith("catchall:")) {
|
||||
def msg = zigbee.parse(description)
|
||||
log.trace msg
|
||||
log.trace "data: $msg.data"
|
||||
|
||||
if(description?.endsWith("0100") ||description?.endsWith("1001"))
|
||||
{
|
||||
def result = createEvent(name: "switch", value: "on")
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
log.debug "description is $description"
|
||||
|
||||
def resultMap = zigbee.getKnownDescription(description)
|
||||
if (resultMap) {
|
||||
log.info resultMap
|
||||
if (resultMap.type == "update") {
|
||||
log.info "$device updates: ${resultMap.value}"
|
||||
}
|
||||
|
||||
if(description?.endsWith("0000") || description?.endsWith("1000"))
|
||||
{
|
||||
def result = createEvent(name: "switch", value: "off")
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
else {
|
||||
sendEvent(name: resultMap.type, value: resultMap.value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (description?.startsWith("read attr")) {
|
||||
|
||||
log.debug description[-2..-1]
|
||||
def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 )
|
||||
|
||||
sendEvent( name: "level", value: i )
|
||||
}
|
||||
|
||||
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug zigbee.parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
zigbee.off()
|
||||
}
|
||||
|
||||
def on() {
|
||||
log.debug "on()"
|
||||
sendEvent(name: "switch", value: "on")
|
||||
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}"
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "off()"
|
||||
sendEvent(name: "switch", value: "off")
|
||||
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
|
||||
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
// Schedule poll every 1 min
|
||||
//schedule("0 */1 * * * ?", poll)
|
||||
//poll()
|
||||
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0"
|
||||
]
|
||||
zigbee.on()
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
log.trace "setLevel($value)"
|
||||
def cmds = []
|
||||
zigbee.setLevel(value)
|
||||
}
|
||||
|
||||
if (value == 0) {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {0000 0000}"
|
||||
}
|
||||
else if (device.latestValue("switch") == "off") {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
|
||||
sendEvent(name: "level", value: value)
|
||||
def level = hex(value * 255/100)
|
||||
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}"
|
||||
|
||||
//log.debug cmds
|
||||
cmds
|
||||
def refresh() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
def configCmds = [
|
||||
|
||||
//Switch Reporting
|
||||
"zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1000",
|
||||
|
||||
//Level Control Reporting
|
||||
"zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 500",
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def uninstalled() {
|
||||
|
||||
log.debug "uninstalled()"
|
||||
|
||||
response("zcl rftd")
|
||||
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
|
||||
|
||||
|
||||
private hex(value, width=2) {
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() < width) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
int i = 0;
|
||||
int j = array.length - 1;
|
||||
byte tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
return array
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
}
|
||||
|
||||
@@ -55,141 +55,136 @@ metadata {
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("indicator", "device.indicatorStatus", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
||||
standardTile("indicator", "device.indicatorStatus", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "when off", action:"indicator.indicatorWhenOn", icon:"st.indicators.lit-when-off"
|
||||
state "when on", action:"indicator.indicatorNever", icon:"st.indicators.lit-when-on"
|
||||
state "never", action:"indicator.indicatorWhenOff", icon:"st.indicators.never-lit"
|
||||
}
|
||||
standardTile("refresh", "device.switch", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
|
||||
standardTile("refresh", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "level", label:'${currentValue} %', unit:"%", backgroundColor:"#ffffff"
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "refresh", "indicator"])
|
||||
details(["switch", "level", "indicator", "refresh"])
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
def item1 = [
|
||||
canBeCurrentState: false,
|
||||
linkText: getLinkText(device),
|
||||
isStateChange: false,
|
||||
displayed: false,
|
||||
descriptionText: description,
|
||||
value: description
|
||||
]
|
||||
def result
|
||||
def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 1])
|
||||
if (cmd) {
|
||||
result = createEvent(cmd, item1)
|
||||
def result = null
|
||||
if (description != "updated") {
|
||||
log.debug "parse() >> zwave.parse($description)"
|
||||
def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 1])
|
||||
if (cmd) {
|
||||
result = zwaveEvent(cmd)
|
||||
}
|
||||
}
|
||||
else {
|
||||
item1.displayed = displayed(description, item1.isStateChange)
|
||||
result = [item1]
|
||||
if (result?.name == 'hail' && hubFirmwareLessThan("000.011.00602")) {
|
||||
result = [result, response(zwave.basicV1.basicGet())]
|
||||
log.debug "Was hailed: requesting state update"
|
||||
} else {
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
}
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
result
|
||||
return result
|
||||
}
|
||||
|
||||
def createEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, Map item1) {
|
||||
def result = doCreateEvent(cmd, item1)
|
||||
for (int i = 0; i < result.size(); i++) {
|
||||
result[i].type = "physical"
|
||||
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
|
||||
dimmerEvents(cmd)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
|
||||
dimmerEvents(cmd)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd) {
|
||||
dimmerEvents(cmd)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd) {
|
||||
dimmerEvents(cmd)
|
||||
}
|
||||
|
||||
private dimmerEvents(physicalgraph.zwave.Command cmd) {
|
||||
def value = (cmd.value ? "on" : "off")
|
||||
def result = [createEvent(name: "switch", value: value)]
|
||||
if (cmd.value && cmd.value <= 100) {
|
||||
result << createEvent(name: "level", value: cmd.value, unit: "%")
|
||||
}
|
||||
result
|
||||
return result
|
||||
}
|
||||
|
||||
def createEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd, Map item1) {
|
||||
def result = doCreateEvent(cmd, item1)
|
||||
for (int i = 0; i < result.size(); i++) {
|
||||
result[i].type = "physical"
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStartLevelChange cmd, Map item1) {
|
||||
[]
|
||||
}
|
||||
|
||||
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd, Map item1) {
|
||||
[response(zwave.basicV1.basicGet())]
|
||||
}
|
||||
|
||||
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd, Map item1) {
|
||||
def result = doCreateEvent(cmd, item1)
|
||||
for (int i = 0; i < result.size(); i++) {
|
||||
result[i].type = "physical"
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd, Map item1) {
|
||||
def result = doCreateEvent(cmd, item1)
|
||||
result[0].descriptionText = "${item1.linkText} is ${item1.value}"
|
||||
result[0].handlerName = cmd.value ? "statusOn" : "statusOff"
|
||||
for (int i = 0; i < result.size(); i++) {
|
||||
result[i].type = "digital"
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
def doCreateEvent(physicalgraph.zwave.Command cmd, Map item1) {
|
||||
def result = [item1]
|
||||
|
||||
item1.name = "switch"
|
||||
item1.value = cmd.value ? "on" : "off"
|
||||
item1.handlerName = item1.value
|
||||
item1.descriptionText = "${item1.linkText} was turned ${item1.value}"
|
||||
item1.canBeCurrentState = true
|
||||
item1.isStateChange = isStateChange(device, item1.name, item1.value)
|
||||
item1.displayed = item1.isStateChange
|
||||
|
||||
if (cmd.value >= 5) {
|
||||
def item2 = new LinkedHashMap(item1)
|
||||
item2.name = "level"
|
||||
item2.value = cmd.value as String
|
||||
item2.unit = "%"
|
||||
item2.descriptionText = "${item1.linkText} dimmed ${item2.value} %"
|
||||
item2.canBeCurrentState = true
|
||||
item2.isStateChange = isStateChange(device, item2.name, item2.value)
|
||||
item2.displayed = false
|
||||
result << item2
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
|
||||
log.debug "ConfigurationReport $cmd"
|
||||
def value = "when off"
|
||||
if (cmd.configurationValue[0] == 1) {value = "when on"}
|
||||
if (cmd.configurationValue[0] == 2) {value = "never"}
|
||||
[name: "indicatorStatus", value: value, display: false]
|
||||
createEvent([name: "indicatorStatus", value: value])
|
||||
}
|
||||
|
||||
def createEvent(physicalgraph.zwave.Command cmd, Map map) {
|
||||
// Handles any Z-Wave commands we aren't interested in
|
||||
log.debug "UNHANDLED COMMAND $cmd"
|
||||
def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) {
|
||||
createEvent([name: "hail", value: "hail", descriptionText: "Switch button was pressed", displayed: false])
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
|
||||
log.debug "manufacturerId: ${cmd.manufacturerId}"
|
||||
log.debug "manufacturerName: ${cmd.manufacturerName}"
|
||||
log.debug "productId: ${cmd.productId}"
|
||||
log.debug "productTypeId: ${cmd.productTypeId}"
|
||||
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
|
||||
updateDataValue("MSR", msr)
|
||||
createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false])
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd) {
|
||||
[createEvent(name:"switch", value:"on"), response(zwave.switchMultilevelV1.switchMultilevelGet().format())]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
// Handles all Z-Wave commands we aren't interested in
|
||||
[:]
|
||||
}
|
||||
|
||||
def on() {
|
||||
log.info "on"
|
||||
delayBetween([zwave.basicV1.basicSet(value: 0xFF).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
|
||||
delayBetween([
|
||||
zwave.basicV1.basicSet(value: 0xFF).format(),
|
||||
zwave.switchMultilevelV1.switchMultilevelGet().format()
|
||||
],5000)
|
||||
}
|
||||
|
||||
def off() {
|
||||
delayBetween ([zwave.basicV1.basicSet(value: 0x00).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
|
||||
delayBetween([
|
||||
zwave.basicV1.basicSet(value: 0x00).format(),
|
||||
zwave.switchMultilevelV1.switchMultilevelGet().format()
|
||||
],5000)
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
log.debug "setLevel >> value: $value"
|
||||
def valueaux = value as Integer
|
||||
def level = Math.min(valueaux, 99)
|
||||
def level = Math.max(Math.min(valueaux, 99), 0)
|
||||
if (level > 0) {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
sendEvent(name: "level", value: level, unit: "%")
|
||||
delayBetween ([zwave.basicV1.basicSet(value: level).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000)
|
||||
}
|
||||
|
||||
def setLevel(value, duration) {
|
||||
log.debug "setLevel >> value: $value, duration: $duration"
|
||||
def valueaux = value as Integer
|
||||
def level = Math.min(valueaux, 99)
|
||||
def level = Math.max(Math.min(valueaux, 99), 0)
|
||||
def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60)
|
||||
zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format()
|
||||
def getStatusDelay = duration < 128 ? (duration*1000)+2000 : (Math.round(duration / 60)*60*1000)+2000
|
||||
delayBetween ([zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format(),
|
||||
zwave.switchMultilevelV1.switchMultilevelGet().format()], getStatusDelay)
|
||||
}
|
||||
|
||||
def poll() {
|
||||
@@ -197,21 +192,27 @@ def poll() {
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
zwave.switchMultilevelV1.switchMultilevelGet().format()
|
||||
log.debug "refresh() is called"
|
||||
def commands = []
|
||||
commands << zwave.switchMultilevelV1.switchMultilevelGet().format()
|
||||
if (getDataValue("MSR") == null) {
|
||||
commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
|
||||
}
|
||||
delayBetween(commands,100)
|
||||
}
|
||||
|
||||
def indicatorWhenOn() {
|
||||
sendEvent(name: "indicatorStatus", value: "when on", display: false)
|
||||
sendEvent(name: "indicatorStatus", value: "when on")
|
||||
zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 3, size: 1).format()
|
||||
}
|
||||
|
||||
def indicatorWhenOff() {
|
||||
sendEvent(name: "indicatorStatus", value: "when off", display: false)
|
||||
sendEvent(name: "indicatorStatus", value: "when off")
|
||||
zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 3, size: 1).format()
|
||||
}
|
||||
|
||||
def indicatorNever() {
|
||||
sendEvent(name: "indicatorStatus", value: "never", display: false)
|
||||
sendEvent(name: "indicatorStatus", value: "never")
|
||||
zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 3, size: 1).format()
|
||||
}
|
||||
|
||||
@@ -222,4 +223,4 @@ def invertSwitch(invert=true) {
|
||||
else {
|
||||
zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 4, size: 1).format()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Ecobee Sensor
|
||||
*
|
||||
* Copyright 2015 Juan Risso
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Ecobee Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Sensor"
|
||||
capability "Temperature Measurement"
|
||||
capability "Motion Sensor"
|
||||
capability "Refresh"
|
||||
capability "Polling"
|
||||
}
|
||||
|
||||
simulator {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
tiles {
|
||||
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||
state("temperature", label:'${currentValue}°', unit:"F",
|
||||
backgroundColors:[
|
||||
[value: 31, color: "#153591"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
standardTile("motion", "device.motion") {
|
||||
state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0")
|
||||
state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff")
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main (["temperature","motion"])
|
||||
details(["temperature","motion","refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "refresh..."
|
||||
poll()
|
||||
}
|
||||
|
||||
void poll() {
|
||||
log.debug "Executing 'poll' using parent SmartApp"
|
||||
parent.pollChildren(this)
|
||||
}
|
||||
|
||||
//generate custom mobile activity feeds event
|
||||
def generateActivityFeedsEvent(notificationMessage) {
|
||||
sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true)
|
||||
}
|
||||
@@ -22,31 +22,31 @@ metadata {
|
||||
capability "Polling"
|
||||
capability "Sensor"
|
||||
capability "Refresh"
|
||||
|
||||
|
||||
command "generateEvent"
|
||||
command "raiseSetpoint"
|
||||
command "lowerSetpoint"
|
||||
command "resumeProgram"
|
||||
command "switchMode"
|
||||
|
||||
|
||||
attribute "thermostatSetpoint","number"
|
||||
attribute "thermostatStatus","string"
|
||||
}
|
||||
|
||||
simulator { }
|
||||
|
||||
tiles {
|
||||
|
||||
tiles {
|
||||
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||
state("temperature", label:'${currentValue}°', unit:"F",
|
||||
backgroundColors:[
|
||||
[value: 31, color: "#153591"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
backgroundColors:[
|
||||
[value: 31, color: "#153591"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
)
|
||||
}
|
||||
standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
|
||||
@@ -54,27 +54,27 @@ metadata {
|
||||
state "heat", action:"switchMode", nextState: "updating", icon: "st.thermostat.heat"
|
||||
state "cool", action:"switchMode", nextState: "updating", icon: "st.thermostat.cool"
|
||||
state "auto", action:"switchMode", nextState: "updating", icon: "st.thermostat.auto"
|
||||
state "auxHeatOnly", action:"switchMode", icon: "st.thermostat.emergency-heat"
|
||||
state "updating", label:"Working", icon: "st.secondary.secondary"
|
||||
state "auxHeatOnly", action:"switchMode", icon: "st.thermostat.emergency-heat"
|
||||
state "updating", label:"Working", icon: "st.secondary.secondary"
|
||||
}
|
||||
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
|
||||
state "auto", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "on"
|
||||
state "on", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "off"
|
||||
state "off", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "circulate"
|
||||
state "off", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "circulate"
|
||||
state "circulate", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "auto"
|
||||
}
|
||||
standardTile("upButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||
state "setpoint", action:"raiseSetpoint", backgroundColor:"#d04e00", icon:"st.thermostat.thermostat-up"
|
||||
standardTile("upButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||
state "setpoint", action:"raiseSetpoint", icon:"st.thermostat.thermostat-up"
|
||||
}
|
||||
valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 1, height: 1, decoration: "flat") {
|
||||
valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 1, height: 1, decoration: "flat") {
|
||||
state "thermostatSetpoint", label:'${currentValue}'
|
||||
}
|
||||
valueTile("currentStatus", "device.thermostatStatus", height: 1, width: 2, decoration: "flat") {
|
||||
state "thermostatStatus", label:'${currentValue}', backgroundColor:"#ffffff"
|
||||
}
|
||||
}
|
||||
standardTile("downButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||
state "setpoint", action:"lowerSetpoint", backgroundColor:"#d04e00", icon:"st.thermostat.thermostat-down"
|
||||
}
|
||||
state "setpoint", action:"lowerSetpoint", icon:"st.thermostat.thermostat-down"
|
||||
}
|
||||
controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) {
|
||||
state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#d04e00"
|
||||
}
|
||||
@@ -91,218 +91,196 @@ metadata {
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
standardTile("resumeProgram", "device.resumeProgram", inactiveLabel: false, decoration: "flat") {
|
||||
state "resume", label:'Resume Program', action:"device.resumeProgram", icon:"st.sonos.play-icon"
|
||||
state "resume", action:"resumeProgram", nextState: "updating", label:'Resume Schedule', icon:"st.samsung.da.oven_ic_send"
|
||||
state "updating", label:"Working", icon: "st.secondary.secondary"
|
||||
}
|
||||
main "temperature"
|
||||
details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh"])
|
||||
details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh"])
|
||||
}
|
||||
|
||||
preferences {
|
||||
input "holdType", "enum", title: "Hold Type", description: "When changing temperature, use Temporary (Until next transition) or Permanent hold (default)", required: false, options:["Temporary", "Permanent"]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
preferences {
|
||||
input "highTemperature", "number", title: "Auto Mode High Temperature:", defaultValue: 80
|
||||
input "lowTemperature", "number", title: "Auto Mode Low Temperature:", defaultValue: 70
|
||||
input name: "holdType", type: "enum", title: "Hold Type", description: "When changing temperature, use Temporary or Permanent hold", required: true, options:["Temporary", "Permanent"]
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
// TODO: handle '' attribute
|
||||
|
||||
|
||||
}
|
||||
|
||||
def refresh()
|
||||
{
|
||||
log.debug "refresh called"
|
||||
poll()
|
||||
log.debug "refresh ended"
|
||||
def refresh() {
|
||||
log.debug "refresh called"
|
||||
poll()
|
||||
log.debug "refresh ended"
|
||||
}
|
||||
|
||||
def go()
|
||||
{
|
||||
log.debug "before:go tile tapped"
|
||||
poll()
|
||||
log.debug "after"
|
||||
}
|
||||
|
||||
|
||||
void poll() {
|
||||
log.debug "Executing 'poll' using parent SmartApp"
|
||||
|
||||
|
||||
def results = parent.pollChild(this)
|
||||
parseEventData(results)
|
||||
generateStatusEvent()
|
||||
generateEvent(results) //parse received message from parent
|
||||
}
|
||||
|
||||
def parseEventData(Map results)
|
||||
{
|
||||
|
||||
def generateEvent(Map results) {
|
||||
log.debug "parsing data $results"
|
||||
if(results)
|
||||
{
|
||||
results.each { name, value ->
|
||||
|
||||
if(results) {
|
||||
results.each { name, value ->
|
||||
|
||||
def linkText = getLinkText(device)
|
||||
def isChange = false
|
||||
def isDisplayed = true
|
||||
|
||||
if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") {
|
||||
isChange = isTemperatureStateChange(device, name, value.toString())
|
||||
isDisplayed = isChange
|
||||
|
||||
sendEvent(
|
||||
name: name,
|
||||
value: value,
|
||||
unit: "F",
|
||||
linkText: linkText,
|
||||
descriptionText: getThermostatDescriptionText(name, value, linkText),
|
||||
handlerName: name,
|
||||
isStateChange: isChange,
|
||||
displayed: isDisplayed)
|
||||
|
||||
}
|
||||
else {
|
||||
isChange = isStateChange(device, name, value.toString())
|
||||
isDisplayed = isChange
|
||||
|
||||
sendEvent(
|
||||
name: name,
|
||||
value: value.toString(),
|
||||
linkText: linkText,
|
||||
descriptionText: getThermostatDescriptionText(name, value, linkText),
|
||||
handlerName: name,
|
||||
isStateChange: isChange,
|
||||
displayed: isDisplayed)
|
||||
|
||||
}
|
||||
}
|
||||
generateSetpointEvent ()
|
||||
generateStatusEvent ()
|
||||
}
|
||||
}
|
||||
def isChange = false
|
||||
def isDisplayed = true
|
||||
def event = [name: name, linkText: linkText, descriptionText: getThermostatDescriptionText(name, value, linkText),
|
||||
handlerName: name]
|
||||
|
||||
void generateEvent(Map results)
|
||||
{
|
||||
log.debug "parsing data $results"
|
||||
if(results)
|
||||
{
|
||||
results.each { name, value ->
|
||||
|
||||
def linkText = getLinkText(device)
|
||||
def isChange = false
|
||||
def isDisplayed = true
|
||||
|
||||
if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") {
|
||||
if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") {
|
||||
def sendValue = value? convertTemperatureIfNeeded(value.toDouble(), "F", 1): value //API return temperature value in F
|
||||
isChange = isTemperatureStateChange(device, name, value.toString())
|
||||
isDisplayed = isChange
|
||||
|
||||
sendEvent(
|
||||
name: name,
|
||||
value: value,
|
||||
unit: "F",
|
||||
linkText: linkText,
|
||||
descriptionText: getThermostatDescriptionText(name, value, linkText),
|
||||
handlerName: name,
|
||||
isStateChange: isChange,
|
||||
displayed: isDisplayed)
|
||||
}
|
||||
else {
|
||||
isChange = isStateChange(device, name, value.toString())
|
||||
isDisplayed = isChange
|
||||
|
||||
sendEvent(
|
||||
name: name,
|
||||
value: value.toString(),
|
||||
linkText: linkText,
|
||||
descriptionText: getThermostatDescriptionText(name, value, linkText),
|
||||
handlerName: name,
|
||||
isStateChange: isChange,
|
||||
displayed: isDisplayed)
|
||||
|
||||
}
|
||||
isDisplayed = isChange
|
||||
event << [value: sendValue, isStateChange: isChange, displayed: isDisplayed]
|
||||
} else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){
|
||||
isChange = isStateChange(device, name, value.toString())
|
||||
event << [value: value.toString(), isStateChange: isChange, displayed: false]
|
||||
} else {
|
||||
isChange = isStateChange(device, name, value.toString())
|
||||
isDisplayed = isChange
|
||||
event << [value: value.toString(), isStateChange: isChange, displayed: isDisplayed]
|
||||
}
|
||||
sendEvent(event)
|
||||
}
|
||||
generateSetpointEvent ()
|
||||
generateStatusEvent()
|
||||
generateSetpointEvent ()
|
||||
generateStatusEvent ()
|
||||
}
|
||||
}
|
||||
|
||||
private getThermostatDescriptionText(name, value, linkText)
|
||||
{
|
||||
if(name == "temperature")
|
||||
{
|
||||
return "$linkText was $value°F"
|
||||
|
||||
//return descriptionText to be shown on mobile activity feed
|
||||
private getThermostatDescriptionText(name, value, linkText) {
|
||||
if(name == "temperature") {
|
||||
return "$linkText temperature is $value°F"
|
||||
|
||||
} else if(name == "heatingSetpoint") {
|
||||
return "heating setpoint is $value°F"
|
||||
|
||||
} else if(name == "coolingSetpoint"){
|
||||
return "cooling setpoint is $value°F"
|
||||
|
||||
} else if (name == "thermostatMode") {
|
||||
return "thermostat mode is ${value}"
|
||||
|
||||
} else if (name == "thermostatFanMode") {
|
||||
return "thermostat fan mode is ${value}"
|
||||
|
||||
} else {
|
||||
return "${name} = ${value}"
|
||||
}
|
||||
else if(name == "heatingSetpoint")
|
||||
{
|
||||
return "latest heating setpoint was $value°F"
|
||||
}
|
||||
|
||||
void setHeatingSetpoint(setpoint) {
|
||||
setHeatingSetpoint(setpoint.toDouble())
|
||||
}
|
||||
|
||||
void setHeatingSetpoint(Double setpoint) {
|
||||
// def mode = device.currentValue("thermostatMode")
|
||||
def heatingSetpoint = setpoint
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint").toDouble()
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
|
||||
//enforce limits of heatingSetpoint
|
||||
if (heatingSetpoint > 79) {
|
||||
heatingSetpoint = 79
|
||||
} else if (heatingSetpoint < 45) {
|
||||
heatingSetpoint = 45
|
||||
}
|
||||
else if(name == "coolingSetpoint")
|
||||
{
|
||||
return "latest cooling setpoint was $value°F"
|
||||
|
||||
//enforce limits of heatingSetpoint vs coolingSetpoint
|
||||
if (heatingSetpoint >= coolingSetpoint) {
|
||||
coolingSetpoint = heatingSetpoint
|
||||
}
|
||||
|
||||
log.debug "Sending setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}"
|
||||
|
||||
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
|
||||
if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId, sendHoldType)) {
|
||||
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
|
||||
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
|
||||
log.debug "Done setHeatingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}"
|
||||
generateSetpointEvent()
|
||||
generateStatusEvent()
|
||||
} else {
|
||||
log.error "Error setHeatingSetpoint(setpoint)" //This error is handled by the connect app
|
||||
}
|
||||
else if (name == "thermostatMode")
|
||||
{
|
||||
return "thermostat mode is ${value}"
|
||||
}
|
||||
else
|
||||
{
|
||||
return "${name} = ${value}"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void setHeatingSetpoint(degreesF) {
|
||||
setHeatingSetpoint(degreesF.toDouble())
|
||||
void setCoolingSetpoint(setpoint) {
|
||||
setCoolingSetpoint(setpoint.toDouble())
|
||||
}
|
||||
|
||||
void setHeatingSetpoint(Double degreesF) {
|
||||
log.debug "setHeatingSetpoint({$degreesF})"
|
||||
sendEvent("name":"heatingSetpoint", "value":degreesF)
|
||||
Double coolingSetpoint = device.currentValue("coolingSetpoint")
|
||||
log.debug "coolingSetpoint: $coolingSetpoint"
|
||||
parent.setHold(this, degreesF, coolingSetpoint)
|
||||
void setCoolingSetpoint(Double setpoint) {
|
||||
// def mode = device.currentValue("thermostatMode")
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint").toDouble()
|
||||
def coolingSetpoint = setpoint
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
|
||||
if (coolingSetpoint > 92) {
|
||||
coolingSetpoint = 92
|
||||
} else if (coolingSetpoint < 65) {
|
||||
coolingSetpoint = 65
|
||||
}
|
||||
|
||||
//enforce limits of heatingSetpoint vs coolingSetpoint
|
||||
if (heatingSetpoint >= coolingSetpoint) {
|
||||
heatingSetpoint = coolingSetpoint
|
||||
}
|
||||
|
||||
log.debug "Sending setCoolingSetpoint> coolingSetpoint: ${coolingSetpoint}, heatingSetpoint: ${heatingSetpoint}"
|
||||
|
||||
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
|
||||
if (parent.setHold (this, heatingSetpoint, coolingSetpoint, deviceId, sendHoldType)) {
|
||||
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
|
||||
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
|
||||
log.debug "Done setCoolingSetpoint>> coolingSetpoint = ${coolingSetpoint}, heatingSetpoint = ${heatingSetpoint}"
|
||||
generateSetpointEvent()
|
||||
generateStatusEvent()
|
||||
} else {
|
||||
log.error "Error setCoolingSetpoint(setpoint)" //This error is handled by the connect app
|
||||
}
|
||||
}
|
||||
|
||||
void setCoolingSetpoint(degreesF) {
|
||||
setCoolingSetpoint(degreesF.toDouble())
|
||||
}
|
||||
void resumeProgram() {
|
||||
|
||||
void setCoolingSetpoint(Double degreesF) {
|
||||
log.debug "setCoolingSetpoint({$degreesF})"
|
||||
sendEvent("name":"coolingSetpoint", "value":degreesF)
|
||||
Double heatingSetpoint = device.currentValue("heatingSetpoint")
|
||||
parent.setHold(this, heatingSetpoint, degreesF)
|
||||
}
|
||||
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)) {
|
||||
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)"
|
||||
}
|
||||
|
||||
def configure() {
|
||||
|
||||
}
|
||||
|
||||
def resumeProgram() {
|
||||
parent.resumeProgram(this)
|
||||
}
|
||||
|
||||
def modes() {
|
||||
if (state.modes) {
|
||||
log.debug "Modes = ${state.modes}"
|
||||
return state.modes
|
||||
}
|
||||
else {
|
||||
state.modes = parent.availableModes(this)
|
||||
log.debug "Modes = ${state.modes}"
|
||||
return state.modes
|
||||
}
|
||||
log.debug "Modes = ${state.modes}"
|
||||
return state.modes
|
||||
}
|
||||
else {
|
||||
state.modes = parent.availableModes(this)
|
||||
log.debug "Modes = ${state.modes}"
|
||||
return state.modes
|
||||
}
|
||||
}
|
||||
|
||||
def fanModes() {
|
||||
["off", "on", "auto", "circulate"]
|
||||
}
|
||||
|
||||
|
||||
def switchMode() {
|
||||
log.debug "in switchMode"
|
||||
def currentMode = device.currentState("thermostatMode")?.value
|
||||
@@ -314,7 +292,7 @@ def switchMode() {
|
||||
}
|
||||
|
||||
def switchToMode(nextMode) {
|
||||
log.debug "In switchToMode = ${nextMode}"
|
||||
log.debug "In switchToMode = ${nextMode}"
|
||||
if (nextMode in modes()) {
|
||||
state.lastTriedMode = nextMode
|
||||
"$nextMode"()
|
||||
@@ -376,300 +354,326 @@ def getDataByName(String name) {
|
||||
|
||||
def setThermostatMode(String value) {
|
||||
log.debug "setThermostatMode({$value})"
|
||||
|
||||
|
||||
}
|
||||
|
||||
def setThermostatFanMode(String value) {
|
||||
|
||||
log.debug "setThermostatFanMode({$value})"
|
||||
|
||||
|
||||
}
|
||||
|
||||
def generateModeEvent(mode) {
|
||||
|
||||
sendEvent(name: "thermostatMode", value: mode, descriptionText: "$device.displayName is in ${mode} mode", displayed: true, isStateChange: true)
|
||||
|
||||
sendEvent(name: "thermostatMode", value: mode, descriptionText: "$device.displayName is in ${mode} mode", displayed: true)
|
||||
}
|
||||
|
||||
def generateFanModeEvent(fanMode) {
|
||||
|
||||
sendEvent(name: "thermostatFanMode", value: fanMode, descriptionText: "$device.displayName fan is in ${mode} mode", displayed: true, isStateChange: true)
|
||||
|
||||
sendEvent(name: "thermostatFanMode", value: fanMode, descriptionText: "$device.displayName fan is in ${mode} mode", displayed: true)
|
||||
}
|
||||
|
||||
def generateOperatingStateEvent(operatingState) {
|
||||
|
||||
sendEvent(name: "thermostatOperatingState", value: operatingState, descriptionText: "$device.displayName is ${operatingState}", displayed: true, isStateChange: true)
|
||||
|
||||
sendEvent(name: "thermostatOperatingState", value: operatingState, descriptionText: "$device.displayName is ${operatingState}", displayed: true)
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "off"
|
||||
generateModeEvent("off")
|
||||
if (parent.setMode (this,"off"))
|
||||
generateModeEvent("off")
|
||||
else {
|
||||
log.debug "Error setting new mode."
|
||||
def currentMode = device.currentState("thermostatMode")?.value
|
||||
generateModeEvent(currentMode) // reset the tile back
|
||||
}
|
||||
generateSetpointEvent()
|
||||
generateStatusEvent()
|
||||
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
if (parent.setMode (this,"off", deviceId))
|
||||
generateModeEvent("off")
|
||||
else {
|
||||
log.debug "Error setting new mode."
|
||||
def currentMode = device.currentState("thermostatMode")?.value
|
||||
generateModeEvent(currentMode) // reset the tile back
|
||||
}
|
||||
generateSetpointEvent()
|
||||
generateStatusEvent()
|
||||
}
|
||||
|
||||
def heat() {
|
||||
log.debug "heat"
|
||||
generateModeEvent("heat")
|
||||
if (parent.setMode (this,"heat"))
|
||||
generateModeEvent("heat")
|
||||
else {
|
||||
log.debug "Error setting new mode."
|
||||
def currentMode = device.currentState("thermostatMode")?.value
|
||||
generateModeEvent(currentMode) // reset the tile back
|
||||
}
|
||||
generateSetpointEvent()
|
||||
generateStatusEvent()
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
if (parent.setMode (this,"heat", deviceId))
|
||||
generateModeEvent("heat")
|
||||
else {
|
||||
log.debug "Error setting new mode."
|
||||
def currentMode = device.currentState("thermostatMode")?.value
|
||||
generateModeEvent(currentMode) // reset the tile back
|
||||
}
|
||||
generateSetpointEvent()
|
||||
generateStatusEvent()
|
||||
}
|
||||
|
||||
def auxHeatOnly() {
|
||||
log.debug "auxHeatOnly"
|
||||
generateModeEvent("auxHeatOnly")
|
||||
if (parent.setMode (this,"auxHeatOnly"))
|
||||
generateModeEvent("auxHeatOnly")
|
||||
else {
|
||||
log.debug "Error setting new mode."
|
||||
def currentMode = device.currentState("thermostatMode")?.value
|
||||
generateModeEvent(currentMode) // reset the tile back
|
||||
}
|
||||
generateSetpointEvent()
|
||||
generateStatusEvent()
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
if (parent.setMode (this,"auxHeatOnly", deviceId))
|
||||
generateModeEvent("auxHeatOnly")
|
||||
else {
|
||||
log.debug "Error setting new mode."
|
||||
def currentMode = device.currentState("thermostatMode")?.value
|
||||
generateModeEvent(currentMode) // reset the tile back
|
||||
}
|
||||
generateSetpointEvent()
|
||||
generateStatusEvent()
|
||||
}
|
||||
|
||||
def cool() {
|
||||
log.debug "cool"
|
||||
generateModeEvent("cool")
|
||||
if (parent.setMode (this,"cool"))
|
||||
generateModeEvent("cool")
|
||||
else {
|
||||
log.debug "Error setting new mode."
|
||||
def currentMode = device.currentState("thermostatMode")?.value
|
||||
generateModeEvent(currentMode) // reset the tile back
|
||||
}
|
||||
generateSetpointEvent()
|
||||
generateStatusEvent()
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
if (parent.setMode (this,"cool", deviceId))
|
||||
generateModeEvent("cool")
|
||||
else {
|
||||
log.debug "Error setting new mode."
|
||||
def currentMode = device.currentState("thermostatMode")?.value
|
||||
generateModeEvent(currentMode) // reset the tile back
|
||||
}
|
||||
generateSetpointEvent()
|
||||
generateStatusEvent()
|
||||
}
|
||||
|
||||
def auto() {
|
||||
log.debug "auto"
|
||||
generateModeEvent("auto")
|
||||
if (parent.setMode (this,"auto"))
|
||||
generateModeEvent("auto")
|
||||
else {
|
||||
log.debug "Error setting new mode."
|
||||
def currentMode = device.currentState("thermostatMode")?.value
|
||||
generateModeEvent(currentMode) // reset the tile back
|
||||
}
|
||||
generateSetpointEvent()
|
||||
generateStatusEvent()
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
if (parent.setMode (this,"auto", deviceId))
|
||||
generateModeEvent("auto")
|
||||
else {
|
||||
log.debug "Error setting new mode."
|
||||
def currentMode = device.currentState("thermostatMode")?.value
|
||||
generateModeEvent(currentMode) // reset the tile back
|
||||
}
|
||||
generateSetpointEvent()
|
||||
generateStatusEvent()
|
||||
}
|
||||
|
||||
def fanOn() {
|
||||
log.debug "fanOn"
|
||||
parent.setFanMode (this,"on")
|
||||
|
||||
// parent.setFanMode (this,"on")
|
||||
|
||||
}
|
||||
|
||||
def fanAuto() {
|
||||
log.debug "fanAuto"
|
||||
parent.setFanMode (this,"auto")
|
||||
|
||||
// parent.setFanMode (this,"auto")
|
||||
|
||||
}
|
||||
|
||||
def fanCirculate() {
|
||||
log.debug "fanCirculate"
|
||||
parent.setFanMode (this,"circulate")
|
||||
|
||||
// parent.setFanMode (this,"circulate")
|
||||
|
||||
}
|
||||
|
||||
def fanOff() {
|
||||
log.debug "fanOff"
|
||||
parent.setFanMode (this,"off")
|
||||
|
||||
// parent.setFanMode (this,"off")
|
||||
|
||||
}
|
||||
|
||||
def generateSetpointEvent() {
|
||||
|
||||
log.debug "Generate SetPoint Event"
|
||||
log.debug "Generate SetPoint Event"
|
||||
|
||||
def mode = device.currentValue("thermostatMode")
|
||||
log.debug "Current Mode = ${mode}"
|
||||
log.debug "Current Mode = ${mode}"
|
||||
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||
log.debug "Heating Setpoint = ${heatingSetpoint}"
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||
log.debug "Heating Setpoint = ${heatingSetpoint}"
|
||||
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||
log.debug "Cooling Setpoint = ${coolingSetpoint}"
|
||||
|
||||
if (mode == "heat") {
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°")
|
||||
|
||||
}
|
||||
else if (mode == "cool") {
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°")
|
||||
log.debug "Cooling Setpoint = ${coolingSetpoint}"
|
||||
|
||||
} else if (mode == "auto") {
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":"Auto")
|
||||
if (mode == "heat") {
|
||||
|
||||
} else if (mode == "off") {
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":"Off")
|
||||
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString())
|
||||
|
||||
} else if (mode == "emergencyHeat") {
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°")
|
||||
}
|
||||
else if (mode == "cool") {
|
||||
|
||||
}
|
||||
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString())
|
||||
|
||||
} else if (mode == "auto") {
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":"Auto")
|
||||
|
||||
} else if (mode == "off") {
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":"Off")
|
||||
|
||||
} else if (mode == "emergencyHeat") {
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString())
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void raiseSetpoint() {
|
||||
|
||||
log.debug "Raise SetPoint"
|
||||
|
||||
def mode = device.currentValue("thermostatMode")
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||
|
||||
log.debug "Current Mode = ${mode}"
|
||||
|
||||
if (mode == "heat") {
|
||||
|
||||
heatingSetpoint++
|
||||
|
||||
if (heatingSetpoint > 99)
|
||||
heatingSetpoint = 99
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°")
|
||||
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
|
||||
|
||||
parent.setHold (this, heatingSetpoint, coolingSetpoint)
|
||||
|
||||
log.debug "New Heating Setpoint = ${heatingSetpoint}"
|
||||
|
||||
}
|
||||
else if (mode == "cool") {
|
||||
|
||||
coolingSetpoint++
|
||||
|
||||
if (coolingSetpoint > 99)
|
||||
coolingSetpoint = 99
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°")
|
||||
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
|
||||
|
||||
parent.setHold (this, heatingSetpoint, coolingSetpoint)
|
||||
|
||||
log.debug "New Cooling Setpoint = ${coolingSetpoint}"
|
||||
|
||||
}
|
||||
generateStatusEvent()
|
||||
def targetvalue
|
||||
|
||||
if (mode == "off" || mode == "auto") {
|
||||
log.warn "this mode: $mode does not allow raiseSetpoint"
|
||||
} else {
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||
def thermostatSetpoint = device.currentValue("thermostatSetpoint").toInteger()
|
||||
log.debug "raiseSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}"
|
||||
|
||||
if (device.latestState('thermostatSetpoint')) {
|
||||
targetvalue = device.latestState('thermostatSetpoint').value as Integer
|
||||
} else {
|
||||
targetvalue = 0
|
||||
}
|
||||
targetvalue = targetvalue + 1
|
||||
|
||||
if (mode == "heat" && targetvalue > 79) {
|
||||
targetvalue = 79
|
||||
} else if (mode == "cool" && targetvalue > 92) {
|
||||
targetvalue = 92
|
||||
}
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":targetvalue, 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//called by tile when user hit raise temperature button on UI
|
||||
void lowerSetpoint() {
|
||||
log.debug "Lower SetPoint"
|
||||
|
||||
def mode = device.currentValue("thermostatMode")
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||
|
||||
log.debug "Current Mode = ${mode}, Current Heating Setpoint = ${heatingSetpoint}, Current Cooling Setpoint = ${coolingSetpoint}"
|
||||
|
||||
if (mode == "heat" || mode == "emergencyHeat") {
|
||||
|
||||
heatingSetpoint--
|
||||
|
||||
if (heatingSetpoint < 32)
|
||||
heatingSetpoint = 32
|
||||
def targetvalue
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°")
|
||||
sendEvent("name":"heatingSetpoint", "value":heatingSetpoint)
|
||||
|
||||
parent.setHold (this, heatingSetpoint, coolingSetpoint)
|
||||
|
||||
log.debug "New Heating Setpoint = ${heatingSetpoint}"
|
||||
|
||||
}
|
||||
else if (mode == "cool") {
|
||||
|
||||
coolingSetpoint--
|
||||
|
||||
if (coolingSetpoint < 32)
|
||||
coolingSetpoint = 32
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°")
|
||||
sendEvent("name":"coolingSetpoint", "value":coolingSetpoint)
|
||||
|
||||
parent.setHold (this, heatingSetpoint, coolingSetpoint)
|
||||
|
||||
log.debug "New Cooling Setpoint = ${coolingSetpoint}"
|
||||
|
||||
}
|
||||
generateStatusEvent()
|
||||
if (mode == "off" || mode == "auto") {
|
||||
log.warn "this mode: $mode does not allow lowerSetpoint"
|
||||
} else {
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||
def thermostatSetpoint = device.currentValue("thermostatSetpoint").toInteger()
|
||||
log.debug "lowerSetpoint() mode = ${mode}, heatingSetpoint: ${heatingSetpoint}, coolingSetpoint:${coolingSetpoint}, thermostatSetpoint:${thermostatSetpoint}"
|
||||
if (device.latestState('thermostatSetpoint')) {
|
||||
targetvalue = device.latestState('thermostatSetpoint').value as Integer
|
||||
} else {
|
||||
targetvalue = 0
|
||||
}
|
||||
targetvalue = targetvalue - 1
|
||||
|
||||
if (mode == "heat" && targetvalue.toInteger() < 45) {
|
||||
targetvalue = 45
|
||||
} else if (mode == "cool" && targetvalue.toInteger() < 65) {
|
||||
targetvalue = 65
|
||||
}
|
||||
|
||||
sendEvent("name":"thermostatSetpoint", "value":targetvalue, 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
|
||||
}
|
||||
}
|
||||
|
||||
//called by raiseSetpoint() and lowerSetpoint()
|
||||
void alterSetpoint(temp) {
|
||||
|
||||
def mode = device.currentValue("thermostatMode")
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||
def deviceId = device.deviceNetworkId.split(/\./).last()
|
||||
|
||||
def targetHeatingSetpoint
|
||||
def targetCoolingSetpoint
|
||||
|
||||
//step1: check thermostatMode, enforce limits before sending request to cloud
|
||||
if (mode == "heat"){
|
||||
if (temp.value > coolingSetpoint){
|
||||
targetHeatingSetpoint = temp.value
|
||||
targetCoolingSetpoint = temp.value
|
||||
} else {
|
||||
targetHeatingSetpoint = temp.value
|
||||
targetCoolingSetpoint = coolingSetpoint
|
||||
}
|
||||
} else if (mode == "cool") {
|
||||
//enforce limits before sending request to cloud
|
||||
if (temp.value < heatingSetpoint){
|
||||
targetHeatingSetpoint = temp.value
|
||||
targetCoolingSetpoint = temp.value
|
||||
} else {
|
||||
targetHeatingSetpoint = heatingSetpoint
|
||||
targetCoolingSetpoint = temp.value
|
||||
}
|
||||
}
|
||||
|
||||
log.debug "alterSetpoint >> in mode ${mode} trying to change heatingSetpoint to ${targetHeatingSetpoint} " +
|
||||
"coolingSetpoint to ${targetCoolingSetpoint} with holdType : ${holdType}"
|
||||
|
||||
def sendHoldType = holdType ? (holdType=="Temporary")? "nextTransition" : (holdType=="Permanent")? "indefinite" : "indefinite" : "indefinite"
|
||||
//step2: call parent.setHold to send http request to 3rd party cloud
|
||||
if (parent.setHold(this, targetHeatingSetpoint, targetCoolingSetpoint, deviceId, sendHoldType)) {
|
||||
sendEvent("name": "thermostatSetpoint", "value": temp.value.toString(), displayed: false)
|
||||
sendEvent("name": "heatingSetpoint", "value": targetHeatingSetpoint)
|
||||
sendEvent("name": "coolingSetpoint", "value": targetCoolingSetpoint)
|
||||
log.debug "alterSetpoint in mode $mode succeed change setpoint to= ${temp.value}"
|
||||
} else {
|
||||
log.error "Error alterSetpoint()"
|
||||
if (mode == "heat"){
|
||||
sendEvent("name": "thermostatSetpoint", "value": heatingSetpoint.toString(), displayed: false)
|
||||
} else if (mode == "cool") {
|
||||
sendEvent("name": "thermostatSetpoint", "value": coolingSetpoint.toString(), displayed: false)
|
||||
}
|
||||
}
|
||||
generateStatusEvent()
|
||||
}
|
||||
|
||||
def generateStatusEvent() {
|
||||
|
||||
def mode = device.currentValue("thermostatMode")
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||
def temperature = device.currentValue("temperature").toInteger()
|
||||
|
||||
def statusText
|
||||
|
||||
log.debug "Generate Status Event for Mode = ${mode}"
|
||||
log.debug "Temperature = ${temperature}"
|
||||
log.debug "Heating set point = ${heatingSetpoint}"
|
||||
log.debug "Cooling set point = ${coolingSetpoint}"
|
||||
log.debug "HVAC Mode = ${mode}"
|
||||
|
||||
if (mode == "heat") {
|
||||
|
||||
if (temperature >= heatingSetpoint)
|
||||
statusText = "Right Now: Idle"
|
||||
else
|
||||
statusText = "Heating to ${heatingSetpoint}° F"
|
||||
|
||||
} else if (mode == "cool") {
|
||||
|
||||
if (temperature <= coolingSetpoint)
|
||||
statusText = "Right Now: Idle"
|
||||
else
|
||||
statusText = "Cooling to ${coolingSetpoint}° F"
|
||||
|
||||
} else if (mode == "auto") {
|
||||
|
||||
statusText = "Right Now: Auto"
|
||||
|
||||
} else if (mode == "off") {
|
||||
|
||||
statusText = "Right Now: Off"
|
||||
|
||||
} else if (mode == "emergencyHeat") {
|
||||
|
||||
statusText = "Emergency Heat"
|
||||
|
||||
} else {
|
||||
|
||||
statusText = "?"
|
||||
|
||||
}
|
||||
log.debug "Generate Status Event = ${statusText}"
|
||||
sendEvent("name":"thermostatStatus", "value":statusText, "description":statusText, displayed: true, isStateChange: true)
|
||||
def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger()
|
||||
def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger()
|
||||
def temperature = device.currentValue("temperature").toInteger()
|
||||
|
||||
def statusText
|
||||
|
||||
log.debug "Generate Status Event for Mode = ${mode}"
|
||||
log.debug "Temperature = ${temperature}"
|
||||
log.debug "Heating set point = ${heatingSetpoint}"
|
||||
log.debug "Cooling set point = ${coolingSetpoint}"
|
||||
log.debug "HVAC Mode = ${mode}"
|
||||
|
||||
if (mode == "heat") {
|
||||
|
||||
if (temperature >= heatingSetpoint)
|
||||
statusText = "Right Now: Idle"
|
||||
else
|
||||
statusText = "Heating to ${heatingSetpoint}° F"
|
||||
|
||||
} else if (mode == "cool") {
|
||||
|
||||
if (temperature <= coolingSetpoint)
|
||||
statusText = "Right Now: Idle"
|
||||
else
|
||||
statusText = "Cooling to ${coolingSetpoint}° F"
|
||||
|
||||
} else if (mode == "auto") {
|
||||
|
||||
statusText = "Right Now: Auto"
|
||||
|
||||
} else if (mode == "off") {
|
||||
|
||||
statusText = "Right Now: Off"
|
||||
|
||||
} else if (mode == "emergencyHeat") {
|
||||
|
||||
statusText = "Emergency Heat"
|
||||
|
||||
} else {
|
||||
|
||||
statusText = "?"
|
||||
|
||||
}
|
||||
log.debug "Generate Status Event = ${statusText}"
|
||||
sendEvent("name":"thermostatStatus", "value":statusText, "description":statusText, displayed: true)
|
||||
}
|
||||
|
||||
//generate custom mobile activity feeds event
|
||||
def generateActivityFeedsEvent(notificationMessage) {
|
||||
sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
/**
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Fibaro Smoke Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Battery" //attributes: battery
|
||||
capability "Configuration" //commands: configure()
|
||||
capability "Sensor"
|
||||
capability "Smoke Detector" //attributes: smoke ("detected","clear","tested")
|
||||
capability "Temperature Measurement" //attributes: temperature
|
||||
attribute "tamper", "enum", ["detected", "clear"]
|
||||
attribute "heatAlarm", "enum", ["overheat detected", "clear", "rapid temperature rise", "underheat detected"]
|
||||
fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x86, 0x72, 0x5A, 0x59, 0x85, 0x73, 0x84, 0x80, 0x71, 0x56, 0x70, 0x31, 0x8E, 0x22, 0x9C, 0x98, 0x7A", outClusters: "0x20, 0x8B"
|
||||
}
|
||||
simulator {
|
||||
//battery
|
||||
for (int i in [0, 5, 10, 15, 50, 99, 100]) {
|
||||
status "battery ${i}%": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate(
|
||||
new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: i)
|
||||
).incomingMessage()
|
||||
}
|
||||
status "battery 100%": "command: 8003, payload: 64"
|
||||
status "battery 5%": "command: 8003, payload: 05"
|
||||
//smoke
|
||||
status "smoke detected": "command: 7105, payload: 01 01"
|
||||
status "smoke clear": "command: 7105, payload: 01 00"
|
||||
status "smoke tested": "command: 7105, payload: 01 03"
|
||||
//temperature
|
||||
for (int i = 0; i <= 100; i += 20) {
|
||||
status "temperature ${i}F": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate(
|
||||
new physicalgraph.zwave.Zwave().sensorMultilevelV5.sensorMultilevelReport(scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1)
|
||||
).incomingMessage()
|
||||
}
|
||||
}
|
||||
preferences {
|
||||
input description: "After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration",
|
||||
title: "Instructions", displayDuringSetup: true, type: "paragraph", element: "paragraph"
|
||||
input description: "Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN",
|
||||
title: "To check smoke detection state", displayDuringSetup: true, type: "paragraph", element: "paragraph"
|
||||
input description: "Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings",
|
||||
title: "Advanced Configuration", displayDuringSetup: true, type: "paragraph", element: "paragraph"
|
||||
input "smokeSensorSensitivity", "enum", title: "Smoke Sensor Sensitivity", options: ["High","Medium","Low"], defaultValue: "${smokeSensorSensitivity}", displayDuringSetup: true
|
||||
input "zwaveNotificationStatus", "enum", title: "Notifications Status", options: ["disabled","casing opened","exceeding temperature threshold", "lack of Z-Wave range", "all notifications"],
|
||||
defaultValue: "${zwaveNotificationStatus}", displayDuringSetup: true
|
||||
input "visualIndicatorNotificationStatus", "enum", title: "Visual Indicator Notifications Status",
|
||||
options: ["disabled","casing opened","exceeding temperature threshold", "lack of Z-Wave range", "all notifications"],
|
||||
defaultValue: "${visualIndicatorNotificationStatus}", displayDuringSetup: true
|
||||
input "soundNotificationStatus", "enum", title: "Sound Notifications Status",
|
||||
options: ["disabled","casing opened","exceeding temperature threshold", "lack of Z-Wave range", "all notifications"],
|
||||
defaultValue: "${soundNotificationStatus}", displayDuringSetup: true
|
||||
input "temperatureReportInterval", "enum", title: "Temperature Report Interval",
|
||||
options: ["reports inactive", "5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${temperatureReportInterval}", displayDuringSetup: true
|
||||
input "temperatureReportHysteresis", "number", title: "Temperature Report Hysteresis", description: "Available settings: 1-100 C", range: "1..100", displayDuringSetup: true
|
||||
input "temperatureThreshold", "number", title: "Overheat Temperature Threshold", description: "Available settings: 0 or 2-100 C", range: "0..100", displayDuringSetup: true
|
||||
input "excessTemperatureSignalingInterval", "enum", title: "Excess Temperature Signaling Interval",
|
||||
options: ["5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${excessTemperatureSignalingInterval}", displayDuringSetup: true
|
||||
input "lackOfZwaveRangeIndicationInterval", "enum", title: "Lack of Z-Wave Range Indication Interval",
|
||||
options: ["5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${lackOfZwaveRangeIndicationInterval}", displayDuringSetup: true
|
||||
}
|
||||
tiles (scale: 2){
|
||||
multiAttributeTile(name:"smoke", type: "lighting", width: 6, height: 4){
|
||||
tileAttribute ("device.smoke", key: "PRIMARY_CONTROL") {
|
||||
attributeState("clear", label:"CLEAR", icon:"st.alarm.smoke.clear", backgroundColor:"#ffffff")
|
||||
attributeState("detected", label:"SMOKE", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13")
|
||||
attributeState("tested", label:"TEST", icon:"st.alarm.smoke.test", backgroundColor:"#e86d13")
|
||||
attributeState("replacement required", label:"REPLACE", icon:"st.alarm.smoke.test", backgroundColor:"#FFFF66")
|
||||
attributeState("unknown", label:"UNKNOWN", icon:"st.alarm.smoke.test", backgroundColor:"#ffffff")
|
||||
}
|
||||
tileAttribute ("device.battery", key: "SECONDARY_CONTROL") {
|
||||
attributeState "battery", label:'Battery: ${currentValue}%', unit:"%"
|
||||
}
|
||||
}
|
||||
valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "battery", label:'${currentValue}% battery', unit:"%"
|
||||
}
|
||||
valueTile("temperature", "device.temperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "temperature", label:'${currentValue}°', unit:"C"
|
||||
}
|
||||
valueTile("heatAlarm", "device.heatAlarm", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "clear", label:'TEMPERATURE OK', backgroundColor:"#ffffff"
|
||||
state "overheat detected", label:'OVERHEAT DETECTED', backgroundColor:"#ffffff"
|
||||
state "rapid temperature rise", label:'RAPID TEMP RISE', backgroundColor:"#ffffff"
|
||||
state "underheat detected", label:'UNDERHEAT DETECTED', backgroundColor:"#ffffff"
|
||||
}
|
||||
valueTile("tamper", "device.tamper", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "clear", label:'NO TAMPER', backgroundColor:"#ffffff"
|
||||
state "detected", label:'TAMPER DETECTED', backgroundColor:"#ffffff"
|
||||
}
|
||||
|
||||
main "smoke"
|
||||
details(["smoke","temperature"])
|
||||
}
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
setConfigured("false") //wait until the next time device wakeup to send configure command
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
log.debug "parse() >> description: $description"
|
||||
def result = null
|
||||
if (description.startsWith("Err 106")) {
|
||||
log.debug "parse() >> Err 106"
|
||||
result = createEvent( name: "secureInclusion", value: "failed", isStateChange: true,
|
||||
descriptionText: "This sensor failed to complete the network security key exchange. " +
|
||||
"If you are unable to control it via SmartThings, you must remove it from your network and add it again.")
|
||||
} else if (description != "updated") {
|
||||
log.debug "parse() >> zwave.parse(description)"
|
||||
def cmd = zwave.parse(description, [0x31: 5, 0x71: 3, 0x84: 1])
|
||||
if (cmd) {
|
||||
result = zwaveEvent(cmd)
|
||||
}
|
||||
}
|
||||
log.debug "After zwaveEvent(cmd) >> Parsed '${description}' to ${result.inspect()}"
|
||||
return result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) {
|
||||
log.info "Executing zwaveEvent 86 (VersionV1): 12 (VersionReport) with cmd: $cmd"
|
||||
def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}"
|
||||
updateDataValue("fw", fw)
|
||||
def text = "$device.displayName: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}"
|
||||
createEvent(descriptionText: text, isStateChange: false)
|
||||
}
|
||||
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||
def map = [ name: "battery", unit: "%" ]
|
||||
if (cmd.batteryLevel == 0xFF) {
|
||||
map.value = 1
|
||||
map.descriptionText = "${device.displayName} battery is low"
|
||||
map.isStateChange = true
|
||||
} else {
|
||||
map.value = cmd.batteryLevel
|
||||
}
|
||||
setConfigured("true") //when battery is reported back meaning configuration is done
|
||||
//Store time of last battery update so we don't ask every wakeup, see WakeUpNotification handler
|
||||
state.lastbatt = now()
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationBusy cmd) {
|
||||
def msg = cmd.status == 0 ? "try again later" :
|
||||
cmd.status == 1 ? "try again in $cmd.waitTime seconds" :
|
||||
cmd.status == 2 ? "request queued" : "sorry"
|
||||
createEvent(displayed: true, descriptionText: "$device.displayName is busy, $msg")
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) {
|
||||
createEvent(displayed: true, descriptionText: "$device.displayName rejected the last request")
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||
setSecured()
|
||||
def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 5, 0x71: 3, 0x84: 1])
|
||||
if (encapsulatedCommand) {
|
||||
log.debug "command: 98 (Security) 81(SecurityMessageEncapsulation) encapsulatedCommand: $encapsulatedCommand"
|
||||
zwaveEvent(encapsulatedCommand)
|
||||
} else {
|
||||
log.warn "Unable to extract encapsulated cmd from $cmd"
|
||||
createEvent(descriptionText: cmd.toString())
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) {
|
||||
log.info "Executing zwaveEvent 98 (SecurityV1): 03 (SecurityCommandsSupportedReport) with cmd: $cmd"
|
||||
setSecured()
|
||||
log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device"
|
||||
if (getDataValue("MSR")?.startsWith("010F-0C02")){
|
||||
response(configure()) //configure device using SmartThings default settings
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) {
|
||||
log.info "Executing zwaveEvent 98 (SecurityV1): 07 (NetworkKeyVerify) with cmd: $cmd (node is securely included)"
|
||||
createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful", isStateChange: true, displayed: true)
|
||||
//after device securely joined the network, call configure() to config device
|
||||
setSecured()
|
||||
log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device"
|
||||
if (getDataValue("MSR")?.startsWith("010F-0C02")){
|
||||
response(configure()) //configure device using SmartThings default settings
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
|
||||
log.info "Executing zwaveEvent 71 (NotificationV3): 05 (NotificationReport) with cmd: $cmd"
|
||||
def result = []
|
||||
if (cmd.notificationType == 7) {
|
||||
switch (cmd.event) {
|
||||
case 0:
|
||||
result << createEvent(name: "tamper", value: "clear", displayed: false)
|
||||
break
|
||||
case 3:
|
||||
result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName casing was opened")
|
||||
break
|
||||
}
|
||||
} else if (cmd.notificationType == 1) { //Smoke Alarm (V2)
|
||||
log.debug "notificationv3.NotificationReport: for Smoke Alarm (V2)"
|
||||
result << smokeAlarmEvent(cmd.event)
|
||||
} else if (cmd.notificationType == 4) { // Heat Alarm (V2)
|
||||
log.debug "notificationv3.NotificationReport: for Heat Alarm (V2)"
|
||||
result << heatAlarmEvent(cmd.event)
|
||||
} else {
|
||||
log.warn "Need to handle this cmd.notificationType: ${cmd.notificationType}"
|
||||
result << createEvent(descriptionText: cmd.toString(), isStateChange: false)
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
def smokeAlarmEvent(value) {
|
||||
log.debug "smokeAlarmEvent(value): $value"
|
||||
def map = [name: "smoke"]
|
||||
if (value == 1 || value == 2) {
|
||||
map.value = "detected"
|
||||
map.descriptionText = "$device.displayName detected smoke"
|
||||
} else if (value == 0) {
|
||||
map.value = "clear"
|
||||
map.descriptionText = "$device.displayName is clear (no smoke)"
|
||||
} else if (value == 3) {
|
||||
map.value = "tested"
|
||||
map.descriptionText = "$device.displayName smoke alarm test"
|
||||
} else if (value == 4) {
|
||||
map.value = "replacement required"
|
||||
map.descriptionText = "$device.displayName replacement required"
|
||||
} else {
|
||||
map.value = "unknown"
|
||||
map.descriptionText = "$device.displayName unknown event"
|
||||
}
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def heatAlarmEvent(value) {
|
||||
log.debug "heatAlarmEvent(value): $value"
|
||||
def map = [name: "heatAlarm"]
|
||||
if (value == 1 || value == 2) {
|
||||
map.value = "overheat detected"
|
||||
map.descriptionText = "$device.displayName overheat detected"
|
||||
} else if (value == 0) {
|
||||
map.value = "clear"
|
||||
map.descriptionText = "$device.displayName heat alarm cleared (no overheat)"
|
||||
} else if (value == 3 || value == 4) {
|
||||
map.value = "rapid temperature rise"
|
||||
map.descriptionText = "$device.displayName rapid temperature rise"
|
||||
} else if (value == 5 || value == 6) {
|
||||
map.value = "underheat detected"
|
||||
map.descriptionText = "$device.displayName underheat detected"
|
||||
} else {
|
||||
map.value = "unknown"
|
||||
map.descriptionText = "$device.displayName unknown event"
|
||||
}
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) {
|
||||
log.info "Executing zwaveEvent 84 (WakeUpV1): 07 (WakeUpNotification) with cmd: $cmd"
|
||||
log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device"
|
||||
def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)]
|
||||
def cmds = []
|
||||
/* check MSR = "manufacturerId-productTypeId" to make sure configuration commands are sent to the right model */
|
||||
if (!isConfigured() && getDataValue("MSR")?.startsWith("010F-0C02")) {
|
||||
result << response(configure()) // configure a newly joined device or joined device with preference update
|
||||
} else {
|
||||
//Only ask for battery if we haven't had a BatteryReport in a while
|
||||
if (!state.lastbatt || (new Date().time) - state.lastbatt > 24*60*60*1000) {
|
||||
log.debug("Device has been configured sending >> batteryGet()")
|
||||
cmds << zwave.securityV1.securityMessageEncapsulation().encapsulate(zwave.batteryV1.batteryGet()).format()
|
||||
cmds << "delay 1200"
|
||||
}
|
||||
log.debug("Device has been configured sending >> wakeUpNoMoreInformation()")
|
||||
cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format()
|
||||
result << response(cmds) //tell device back to sleep
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) {
|
||||
log.info "Executing zwaveEvent 31 (SensorMultilevelV5): 05 (SensorMultilevelReport) with cmd: $cmd"
|
||||
def map = [:]
|
||||
switch (cmd.sensorType) {
|
||||
case 1:
|
||||
map.name = "temperature"
|
||||
def cmdScale = cmd.scale == 1 ? "F" : "C"
|
||||
map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision)
|
||||
map.unit = getTemperatureScale()
|
||||
break
|
||||
default:
|
||||
map.descriptionText = cmd.toString()
|
||||
}
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) {
|
||||
log.info "Executing zwaveEvent 5A (DeviceResetLocallyV1) : 01 (DeviceResetLocallyNotification) with cmd: $cmd"
|
||||
createEvent(descriptionText: cmd.toString(), isStateChange: true, displayed: true)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
|
||||
log.info "Executing zwaveEvent 72 (ManufacturerSpecificV2) : 05 (ManufacturerSpecificReport) with cmd: $cmd"
|
||||
log.debug "manufacturerId: ${cmd.manufacturerId}"
|
||||
log.debug "manufacturerName: ${cmd.manufacturerName}"
|
||||
log.debug "productId: ${cmd.productId}"
|
||||
log.debug "productTypeId: ${cmd.productTypeId}"
|
||||
def result = []
|
||||
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
|
||||
updateDataValue("MSR", msr)
|
||||
log.debug "After device is securely joined, send commands to update tiles"
|
||||
result << zwave.batteryV1.batteryGet()
|
||||
result << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01)
|
||||
result << zwave.wakeUpV1.wakeUpNoMoreInformation()
|
||||
[[descriptionText:"${device.displayName} MSR report"], response(commands(result, 5000))]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) {
|
||||
def result = []
|
||||
if (cmd.nodeId.any { it == zwaveHubNodeId }) {
|
||||
result << createEvent(descriptionText: "$device.displayName is associated in group ${cmd.groupingIdentifier}")
|
||||
} else if (cmd.groupingIdentifier == 1) {
|
||||
result << createEvent(descriptionText: "Associating $device.displayName in group ${cmd.groupingIdentifier}")
|
||||
result << response(zwave.associationV1.associationSet(groupingIdentifier:cmd.groupingIdentifier, nodeId:zwaveHubNodeId))
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
log.warn "General zwaveEvent cmd: ${cmd}"
|
||||
createEvent(descriptionText: cmd.toString(), isStateChange: false)
|
||||
}
|
||||
|
||||
def configure() {
|
||||
// This sensor joins as a secure device if you tripple-click the button to include it
|
||||
log.debug "configure() >> isSecured() : ${isSecured()}"
|
||||
if (!isSecured()) {
|
||||
log.debug "Fibaro smoke sensor not sending configure until secure"
|
||||
return []
|
||||
} else {
|
||||
log.info "${device.displayName} is configuring its settings"
|
||||
def request = []
|
||||
|
||||
//1. configure wakeup interval : available: 0, 4200s-65535s, device default 21600s(6hr)
|
||||
request += zwave.wakeUpV1.wakeUpIntervalSet(seconds:6*3600, nodeid:zwaveHubNodeId)
|
||||
|
||||
//2. Smoke Sensitivity 3 levels: 1-HIGH , 2-MEDIUM (default), 3-LOW
|
||||
if (smokeSensorSensitivity && smokeSensorSensitivity != "null") {
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 1, size: 1,
|
||||
scaledConfigurationValue:
|
||||
smokeSensorSensitivity == "High" ? 1 :
|
||||
smokeSensorSensitivity == "Medium" ? 2 :
|
||||
smokeSensorSensitivity == "Low" ? 3 : 2)
|
||||
}
|
||||
//3. Z-Wave notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable
|
||||
if (zwaveNotificationStatus && zwaveNotificationStatus != "null"){
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: notificationOptionValueMap[zwaveNotificationStatus] ?: 0)
|
||||
}
|
||||
//4. Visual indicator notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable, 4-lack of range notification
|
||||
if (visualIndicatorNotificationStatus && visualIndicatorNotificationStatus != "null") {
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: notificationOptionValueMap[visualIndicatorNotificationStatus] ?: 0)
|
||||
}
|
||||
//5. Sound notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable, 4-lack of range notification
|
||||
if (soundNotificationStatus && soundNotificationStatus != "null") {
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, scaledConfigurationValue: notificationOptionValueMap[soundNotificationStatus] ?: 0)
|
||||
}
|
||||
//6. Temperature report interval: 0-report inactive, 1-8640 (multiply by 10 secs) [10s-24hr], default 180 (30 minutes)
|
||||
if (temperatureReportInterval && temperatureReportInterval != "null") {
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 20, size: 2, scaledConfigurationValue: timeOptionValueMap[temperatureReportInterval] ?: 180)
|
||||
} else { //send SmartThings default configuration
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 20, size: 2, scaledConfigurationValue: 180)
|
||||
}
|
||||
//7. Temperature report hysteresis: 1-100 (in 0.1C step) [0.1C - 10C], default 10 (1 C)
|
||||
if (temperatureReportHysteresis && temperatureReportHysteresis != null) {
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 21, size: 1, scaledConfigurationValue: temperatureReportHysteresis < 1 ? 1 : temperatureReportHysteresis > 100 ? 100 : temperatureReportHysteresis)
|
||||
}
|
||||
//8. Temperature threshold: 1-100 (C), default 55 (C)
|
||||
if (temperatureThreshold && temperatureThreshold != null) {
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 30, size: 1, scaledConfigurationValue: temperatureThreshold < 1 ? 1 : temperatureThreshold > 100 ? 100 : temperatureThreshold)
|
||||
}
|
||||
//9. Excess temperature signaling interval: 1-8640 (multiply by 10 secs) [10s-24hr], default 180 (30 minutes)
|
||||
if (excessTemperatureSignalingInterval && excessTemperatureSignalingInterval != "null") {
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 31, size: 2, scaledConfigurationValue: timeOptionValueMap[excessTemperatureSignalingInterval] ?: 180)
|
||||
} else { //send SmartThings default configuration
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 31, size: 2, scaledConfigurationValue: 180)
|
||||
}
|
||||
//10. Lack of Z-Wave range indication interval: 1-8640 (multiply by 10 secs) [10s-24hr], default 2160 (6 hours)
|
||||
if (lackOfZwaveRangeIndicationInterval && lackOfZwaveRangeIndicationInterval != "null") {
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 32, size: 2, scaledConfigurationValue: timeOptionValueMap[lackOfZwaveRangeIndicationInterval] ?: 2160)
|
||||
} else {
|
||||
request += zwave.configurationV1.configurationSet(parameterNumber: 32, size: 2, scaledConfigurationValue: 2160)
|
||||
}
|
||||
//11. get battery level when device is paired
|
||||
request += zwave.batteryV1.batteryGet()
|
||||
|
||||
//12. get temperature reading from device
|
||||
request += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01)
|
||||
|
||||
commands(request) + ["delay 10000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]
|
||||
}
|
||||
}
|
||||
|
||||
private def getTimeOptionValueMap() { [
|
||||
"5 minutes" : 30,
|
||||
"15 minutes" : 90,
|
||||
"30 minutes" : 180,
|
||||
"1 hour" : 360,
|
||||
"6 hours" : 2160,
|
||||
"12 hours" : 4320,
|
||||
"18 hours" : 6480,
|
||||
"24 hours" : 8640,
|
||||
"reports inactive" : 0,
|
||||
]}
|
||||
|
||||
private def getNotificationOptionValueMap() { [
|
||||
"disabled" : 0,
|
||||
"casing opened" : 1,
|
||||
"exceeding temperature threshold" : 2,
|
||||
"lack of Z-Wave range" : 4,
|
||||
"all notifications" : 7,
|
||||
]}
|
||||
|
||||
private command(physicalgraph.zwave.Command cmd) {
|
||||
if (isSecured()) {
|
||||
log.info "Sending secured command: ${cmd}"
|
||||
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||
} else {
|
||||
log.info "Sending unsecured command: ${cmd}"
|
||||
cmd.format()
|
||||
}
|
||||
}
|
||||
|
||||
private commands(commands, delay=200) {
|
||||
log.info "inside commands: ${commands}"
|
||||
delayBetween(commands.collect{ command(it) }, delay)
|
||||
}
|
||||
|
||||
private setConfigured(configure) {
|
||||
updateDataValue("configured", configure)
|
||||
}
|
||||
private isConfigured() {
|
||||
getDataValue("configured") == "true"
|
||||
}
|
||||
private setSecured() {
|
||||
updateDataValue("secured", "true")
|
||||
}
|
||||
private isSecured() {
|
||||
getDataValue("secured") == "true"
|
||||
}
|
||||
@@ -0,0 +1,887 @@
|
||||
/**
|
||||
* Fidure Thermostat, Based on ZigBee thermostat (SmartThings)
|
||||
*
|
||||
* Author: Fidure
|
||||
* Date: 2014-12-13
|
||||
* Updated: 2015-08-26
|
||||
*/
|
||||
metadata {
|
||||
// Automatically generated. Make future change here.
|
||||
definition (name: "Fidure Thermostat", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
capability "Actuator"
|
||||
capability "Temperature Measurement"
|
||||
capability "Thermostat"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Polling"
|
||||
|
||||
attribute "displayTemperature","number"
|
||||
attribute "displaySetpoint", "string"
|
||||
command "raiseSetpoint"
|
||||
command "lowerSetpoint"
|
||||
attribute "upButtonState", "string"
|
||||
attribute "downButtonState", "string"
|
||||
|
||||
attribute "runningMode", "string"
|
||||
attribute "lockLevel", "string"
|
||||
|
||||
command "setThermostatTime"
|
||||
command "lock"
|
||||
|
||||
attribute "prorgammingOperation", "number"
|
||||
attribute "prorgammingOperationDisplay", "string"
|
||||
command "Program"
|
||||
|
||||
attribute "setpointHold", "string"
|
||||
attribute "setpointHoldDisplay", "string"
|
||||
command "Hold"
|
||||
attribute "holdExpiary", "string"
|
||||
|
||||
attribute "lastTimeSync", "string"
|
||||
|
||||
attribute "thermostatOperatingState", "string"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0201,0204,0B05", outClusters: "000A, 0019"
|
||||
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator { }
|
||||
// pref
|
||||
preferences {
|
||||
|
||||
input ("hold_time", "enum", title: "Default Hold Time in Hours",
|
||||
description: "Default Hold Duration in hours",
|
||||
range: "1..24", options: ["No Hold", "2 Hours", "4 Hours", "8 Hours", "12 Hours", "1 Day"],
|
||||
displayDuringSetup: false)
|
||||
input ("sync_clock", "boolean", title: "Synchronize Thermostat Clock Automatically?", options: ["Yes","No"])
|
||||
input ("lock_level", "enum", title: "Thermostat Screen Lock Level", options: ["Full","Mode Only", "Setpoint"])
|
||||
}
|
||||
|
||||
tiles {
|
||||
valueTile("temperature", "displayTemperature", width: 2, height: 2) {
|
||||
state("temperature", label:'${currentValue}°', unit:"F",
|
||||
backgroundColors:[
|
||||
[value: 0, color: "#153591"],
|
||||
[value: 7, color: "#1e9cbb"],
|
||||
[value: 15, color: "#90d2a7"],
|
||||
[value: 23, color: "#44b621"],
|
||||
[value: 29, color: "#f1d801"],
|
||||
[value: 35, color: "#d04e00"],
|
||||
[value: 36, color: "#bc2323"],
|
||||
// fahrenheit range
|
||||
[value: 37, color: "#153591"],
|
||||
[value: 44, color: "#1e9cbb"],
|
||||
[value: 59, color: "#90d2a7"],
|
||||
[value: 74, color: "#44b621"],
|
||||
[value: 84, color: "#f1d801"],
|
||||
[value: 95, color: "#d04e00"],
|
||||
[value: 96, color: "#bc2323"]
|
||||
]
|
||||
)
|
||||
}
|
||||
standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
|
||||
state "off", action:"thermostat.setThermostatMode", icon:"st.thermostat.heating-cooling-off"
|
||||
state "cool", action:"thermostat.setThermostatMode", icon:"st.thermostat.cool"
|
||||
state "heat", action:"thermostat.setThermostatMode", icon:"st.thermostat.heat"
|
||||
state "auto", action:"thermostat.setThermostatMode", icon:"st.thermostat.auto"
|
||||
}
|
||||
|
||||
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
|
||||
state "fanAuto", label:'${name}', action:"thermostat.setThermostatFanMode"
|
||||
state "fanOn", label:'${name}', action:"thermostat.setThermostatFanMode"
|
||||
}
|
||||
|
||||
standardTile("hvacStatus", "thermostatOperatingState", inactiveLabel: false, decoration: "flat") {
|
||||
state "Resting", label: 'Resting'
|
||||
state "Heating", icon:"st.thermostat.heating"
|
||||
state "Cooling", icon:"st.thermostat.cooling"
|
||||
}
|
||||
|
||||
|
||||
standardTile("lock", "lockLevel", inactiveLabel: false, decoration: "flat") {
|
||||
state "Unlocked", action:"lock", label:'${name}'
|
||||
state "Mode Only", action:"lock", label:'${name}'
|
||||
state "Setpoint", action:"lock", label:'${name}'
|
||||
state "Full", action:"lock", label:'${name}'
|
||||
}
|
||||
|
||||
controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false, range: "$min..$max") {
|
||||
state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#d04e00"
|
||||
}
|
||||
valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||
state "heat", label:'${currentValue}° heat', unit:"F", backgroundColor:"#ffffff"
|
||||
}
|
||||
controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false, range: "$min..$max") {
|
||||
state "setCoolingSetpoint", action:"thermostat.setCoolingSetpoint", backgroundColor: "#1e9cbb"
|
||||
}
|
||||
valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||
state "cool", label:'${currentValue}° cool', unit:"F", backgroundColor:"#ffffff"
|
||||
}
|
||||
standardTile("refresh", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
|
||||
state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||
}
|
||||
|
||||
valueTile("scheduleText", "prorgammingOperation", inactiveLabel: false, decoration: "flat", width: 2) {
|
||||
state "default", label: 'Schedule'
|
||||
}
|
||||
valueTile("schedule", "prorgammingOperationDisplay", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", action:"Program", label: '${currentValue}'
|
||||
}
|
||||
|
||||
valueTile("hold", "setpointHoldDisplay", inactiveLabel: false, decoration: "flat", width: 3) {
|
||||
state "setpointHold", action:"Hold", label: '${currentValue}'
|
||||
}
|
||||
|
||||
valueTile("setpoint", "displaySetpoint", width: 2, height: 2)
|
||||
{
|
||||
state("displaySetpoint", label: '${currentValue}°',
|
||||
backgroundColor: "#919191")
|
||||
}
|
||||
|
||||
standardTile("upButton", "upButtonState", decoration: "flat", inactiveLabel: false) {
|
||||
state "normal", action:"raiseSetpoint", backgroundColor:"#919191", icon:"st.thermostat.thermostat-up"
|
||||
state "pressed", action:"raiseSetpoint", backgroundColor:"#ff0000", icon:"st.thermostat.thermostat-up"
|
||||
}
|
||||
standardTile("downButton", "downButtonState", decoration: "flat", inactiveLabel: false) {
|
||||
state "normal", action:"lowerSetpoint", backgroundColor:"#919191", icon:"st.thermostat.thermostat-down"
|
||||
state "pressed", action:"lowerSetpoint", backgroundColor:"#ff9191", icon:"st.thermostat.thermostat-down"
|
||||
}
|
||||
|
||||
|
||||
main "temperature"
|
||||
details([ "temperature", "mode", "hvacStatus","setpoint","upButton","downButton","scheduleText", "schedule", "hold",
|
||||
"heatSliderControl", "heatingSetpoint","coolSliderControl", "coolingSetpoint", "lock", "refresh", "configure"])
|
||||
}
|
||||
}
|
||||
|
||||
def getMin() {
|
||||
try
|
||||
{
|
||||
if (getTemperatureScale() == "C")
|
||||
return 10
|
||||
else
|
||||
return 50
|
||||
} catch (all)
|
||||
{
|
||||
return 10
|
||||
}
|
||||
}
|
||||
|
||||
def getMax() {
|
||||
try {
|
||||
if (getTemperatureScale() == "C")
|
||||
return 30
|
||||
else
|
||||
return 86
|
||||
} catch (all)
|
||||
{
|
||||
return 86
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parse description $description"
|
||||
def result = []
|
||||
|
||||
if (description?.startsWith("read attr -")) {
|
||||
|
||||
//TODO: Parse RAW strings for multiple attributes
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
log.debug "Desc Map: $descMap"
|
||||
for ( atMap in descMap.attrs)
|
||||
{
|
||||
def map = [:]
|
||||
|
||||
if (descMap.cluster == "0201")
|
||||
{
|
||||
//log.trace "attribute: ${atMap.attrId} "
|
||||
switch(atMap.attrId.toLowerCase())
|
||||
{
|
||||
case "0000":
|
||||
map.name = "temperature"
|
||||
map.value = getTemperature(atMap.value)
|
||||
result += createEvent("name":"displayTemperature", "value": getDisplayTemperature(atMap.value))
|
||||
break;
|
||||
case "0005":
|
||||
//log.debug "hex time: ${descMap.value}"
|
||||
if (atMap.encoding == "23")
|
||||
{
|
||||
map.name = "holdExpiary"
|
||||
map.value = "${convertToTime(atMap.value).getTime()}"
|
||||
//log.trace "HOLD EXPIRY: ${atMap.value} is ${map.value}"
|
||||
updateHoldLabel("HoldExp", "${map.value}")
|
||||
}
|
||||
break;
|
||||
case "0011":
|
||||
map.name = "coolingSetpoint"
|
||||
map.value = getDisplayTemperature(atMap.value)
|
||||
updateSetpoint(map.name,map.value)
|
||||
break;
|
||||
case "0012":
|
||||
map.name = "heatingSetpoint"
|
||||
map.value = getDisplayTemperature(atMap.value)
|
||||
updateSetpoint(map.name,map.value)
|
||||
break;
|
||||
case "001c":
|
||||
map.name = "thermostatMode"
|
||||
map.value = getModeMap()[atMap.value]
|
||||
updateSetpoint(map.name,map.value)
|
||||
break;
|
||||
case "001e": //running mode enum8
|
||||
map.name = "runningMode"
|
||||
map.value = getModeMap()[atMap.value]
|
||||
updateSetpoint(map.name,map.value)
|
||||
break;
|
||||
case "0023": // setpoint hold enum8
|
||||
map.name = "setpointHold"
|
||||
map.value = getHoldMap()[atMap.value]
|
||||
updateHoldLabel("Hold", map.value)
|
||||
break;
|
||||
case "0024": // hold duration int16u
|
||||
map.name = "setpointHoldDuration"
|
||||
map.value = Integer.parseInt("${atMap.value}", 16)
|
||||
|
||||
break;
|
||||
case "0025": // thermostat programming operation bitmap8
|
||||
map.name = "prorgammingOperation"
|
||||
def val = getProgrammingMap()[Integer.parseInt("${atMap.value}", 16) & 0x01]
|
||||
result += createEvent("name":"prorgammingOperationDisplay", "value": val)
|
||||
map.value = atMap.value
|
||||
break;
|
||||
case "0029":
|
||||
// relay state
|
||||
map.name = "thermostatOperatingState"
|
||||
map.value = getThermostatOperatingState(atMap.value)
|
||||
break;
|
||||
}
|
||||
} else if (descMap.cluster == "0204")
|
||||
{
|
||||
if (atMap.attrId == "0001")
|
||||
{
|
||||
map.name = "lockLevel"
|
||||
map.value = getLockMap()[atMap.value]
|
||||
}
|
||||
}
|
||||
|
||||
if (map) {
|
||||
result += createEvent(map)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.debug "Parse returned $result"
|
||||
return result
|
||||
}
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
def map = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
|
||||
def attrId = map.get('attrId')
|
||||
def encoding = map.get('encoding')
|
||||
def value = map.get('value')
|
||||
def result = map.get('result')
|
||||
def list = [];
|
||||
|
||||
if (getDataLengthByType(map.get('encoding')) < map.get('value').length()) {
|
||||
def raw = map.get('raw')
|
||||
|
||||
def size = Long.parseLong(''+ map.get('size'), 16)
|
||||
def index = 12;
|
||||
def len
|
||||
|
||||
//log.trace "processing multi attributes"
|
||||
while((index-12) < size) {
|
||||
attrId = flipHexStringEndianness(raw[index..(index+3)])
|
||||
index+= 4;
|
||||
if (result == "success")
|
||||
index+=2;
|
||||
encoding = raw[index..(index+1)]
|
||||
index+= 2;
|
||||
len =getDataLengthByType(encoding)
|
||||
value = flipHexStringEndianness(raw[index..(index+len-1)])
|
||||
index+=len;
|
||||
list += ['attrId': "$attrId", 'encoding':"$encoding", 'value': "$value"]
|
||||
}
|
||||
}
|
||||
else
|
||||
list += ['attrId': "$attrId", 'encoding': "$encoding", 'value': "$value"]
|
||||
|
||||
map.remove('value')
|
||||
map.remove('encoding')
|
||||
map.remove('attrId')
|
||||
map += ['attrs' : list ]
|
||||
}
|
||||
|
||||
def flipHexStringEndianness(s)
|
||||
{
|
||||
s = s.reverse()
|
||||
def sb = new StringBuilder()
|
||||
for (int i=0; i < s.length() -1; i+=2)
|
||||
sb.append(s.charAt(i+1)).append(s.charAt(i))
|
||||
sb
|
||||
}
|
||||
|
||||
def getDataLengthByType(t)
|
||||
{
|
||||
// number of bytes in each static data type
|
||||
def map = ["08":1, "09":2, "0a":3, "0b":4, "0c":5, "0d":6, "0e":7, "0f":8, "10":1, "18":1, "19":2, "1a":3, "1b":4,
|
||||
"1c":5,"1d":6, "1e":7, "1f":8, "20":1, "21":2, "22":3, "23":4, "24":5, "25":6, "26":7, "27":8, "28":1, "29":2,
|
||||
"2a":3, "2b":4, "2c":5, "2d":6, "2e":7, "2f":8, "30":1, "31":2, "38":2, "39":4, "40":8, "e0":4, "e1":4, "e2":4,
|
||||
"e8":2, "e9":2, "ea":4, "f0":8, "f1":16]
|
||||
|
||||
// return number of hex chars
|
||||
return map.get(t) * 2
|
||||
}
|
||||
|
||||
|
||||
def getProgrammingMap() { [
|
||||
0:"Off",
|
||||
1:"On"
|
||||
]}
|
||||
|
||||
def getModeMap() { [
|
||||
"00":"off",
|
||||
"01":"auto",
|
||||
"03":"cool",
|
||||
"04":"heat"
|
||||
]}
|
||||
|
||||
def getFanModeMap() { [
|
||||
"04":"fanOn",
|
||||
"05":"fanAuto"
|
||||
]}
|
||||
|
||||
def getHoldMap()
|
||||
{[
|
||||
"00":"Off",
|
||||
"01":"On"
|
||||
]}
|
||||
|
||||
|
||||
def updateSetpoint(attrib, val)
|
||||
{
|
||||
def cool = device.currentState("coolingSetpoint")?.value
|
||||
def heat = device.currentState("heatingSetpoint")?.value
|
||||
def runningMode = device.currentState("runningMode")?.value
|
||||
def mode = device.currentState("thermostatMode")?.value
|
||||
|
||||
def value = '--';
|
||||
|
||||
|
||||
if ("heat" == mode && heat != null)
|
||||
value = heat;
|
||||
else if ("cool" == mode && cool != null)
|
||||
value = cool;
|
||||
else if ("auto" == mode && runningMode == "cool" && cool != null)
|
||||
value = cool;
|
||||
else if ("auto" == mode && runningMode == "heat" && heat != null)
|
||||
value = heat;
|
||||
|
||||
sendEvent("name":"displaySetpoint", "value": value)
|
||||
}
|
||||
|
||||
def raiseSetpoint()
|
||||
{
|
||||
sendEvent("name":"upButtonState", "value": "pressed")
|
||||
sendEvent("name":"upButtonState", "value": "normal")
|
||||
adjustSetpoint(5)
|
||||
}
|
||||
|
||||
def lowerSetpoint()
|
||||
{
|
||||
sendEvent("name":"downButtonState", "value": "pressed")
|
||||
sendEvent("name":"downButtonState", "value": "normal")
|
||||
adjustSetpoint(-5)
|
||||
}
|
||||
|
||||
def adjustSetpoint(value)
|
||||
{
|
||||
def runningMode = device.currentState("runningMode")?.value
|
||||
def mode = device.currentState("thermostatMode")?.value
|
||||
|
||||
//default to both heat and cool
|
||||
def modeData = 0x02
|
||||
|
||||
if ("heat" == mode || "heat" == runningMode)
|
||||
modeData = "00"
|
||||
else if ("cool" == mode || "cool" == runningMode)
|
||||
modeData = "01"
|
||||
|
||||
def amountData = String.format("%02X", value)[-2..-1]
|
||||
|
||||
|
||||
"st cmd 0x${device.deviceNetworkId} 1 0x201 0 {" + modeData + " " + amountData + "}"
|
||||
|
||||
}
|
||||
|
||||
|
||||
def getDisplayTemperature(value)
|
||||
{
|
||||
def t = Integer.parseInt("$value", 16);
|
||||
|
||||
|
||||
if (getTemperatureScale() == "C") {
|
||||
t = (((t + 4) / 10) as Integer) / 10;
|
||||
} else {
|
||||
t = ((10 *celsiusToFahrenheit(t/100)) as Integer)/ 10;
|
||||
}
|
||||
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
def updateHoldLabel(attr, value)
|
||||
{
|
||||
def currentHold = (device?.currentState("setpointHold")?.value)?: "..."
|
||||
|
||||
def holdExp = device?.currentState("holdExpiary")?.value
|
||||
holdExp = holdExp?: "${(new Date()).getTime()}"
|
||||
|
||||
if ("Hold" == attr)
|
||||
{
|
||||
currentHold = value
|
||||
}
|
||||
|
||||
if ("HoldExp" == attr)
|
||||
{
|
||||
holdExp = value
|
||||
}
|
||||
boolean past = ( (new Date(holdExp.toLong()).getTime()) < (new Date().getTime()))
|
||||
|
||||
if ("HoldExp" == attr)
|
||||
{
|
||||
if (!past)
|
||||
currentHold = "On"
|
||||
else
|
||||
currentHold = "Off"
|
||||
}
|
||||
|
||||
def holdString = (currentHold == "On")?
|
||||
( (past)? "Is On" : "Ends ${compareWithNow(holdExp.toLong())}") :
|
||||
((currentHold == "Off")? " is Off" : " ...")
|
||||
|
||||
sendEvent("name":"setpointHoldDisplay", "value": "Hold ${holdString}")
|
||||
}
|
||||
|
||||
def getSetPointHoldDuration()
|
||||
{
|
||||
def holdTime = 0
|
||||
|
||||
if (settings.hold_time?.contains("Hours"))
|
||||
{
|
||||
holdTime = Integer.parseInt(settings.hold_time[0..1].trim())
|
||||
}
|
||||
else if (settings.hold_time?.contains("Day"))
|
||||
{
|
||||
holdTime = Integer.parseInt(settings.hold_time[0..1].trim()) * 24
|
||||
}
|
||||
|
||||
def currentHoldDuration = device.currentState("setpointHoldDuration")?.value
|
||||
|
||||
|
||||
if (Short.parseShort('0'+ (currentHoldDuration?: 0)) != (holdTime * 60))
|
||||
{
|
||||
[
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x24 0x21 {" +
|
||||
String.format("%04X", ((holdTime * 60) as Short)) // switch to zigbee endian
|
||||
|
||||
+ "}", "delay 100",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x201 0x24", "delay 200",
|
||||
]
|
||||
|
||||
} else
|
||||
{
|
||||
[]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def Hold()
|
||||
{
|
||||
def currentHold = device.currentState("setpointHold")?.value
|
||||
|
||||
def next = (currentHold == "On") ? "00" : "01"
|
||||
def nextHold = getHoldMap()[next]
|
||||
|
||||
sendEvent("name":"setpointHold", "value":nextHold)
|
||||
|
||||
// set the duration first if it's changed
|
||||
|
||||
[
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x23 0x30 {$next}", "delay 100" ,
|
||||
|
||||
"raw 0x201 {04 21 11 00 00 05 00 }","delay 200", // hold expiry time
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
] + getSetPointHoldDuration()
|
||||
}
|
||||
|
||||
def compareWithNow(d)
|
||||
{
|
||||
long mins = (new Date(d)).getTime() - (new Date()).getTime()
|
||||
|
||||
mins /= 1000 * 60;
|
||||
|
||||
log.trace "mins: ${mins}"
|
||||
|
||||
boolean past = (mins < 0)
|
||||
def ret = (past)? "" : "in "
|
||||
|
||||
if (past)
|
||||
mins *= -1;
|
||||
|
||||
float t = 0;
|
||||
// minutes
|
||||
if (mins < 60)
|
||||
{
|
||||
ret += (mins as Integer) + " min" + ((mins > 1)? 's' : '')
|
||||
}else if (mins < 1440)
|
||||
{
|
||||
t = ( Math.round((14 + mins)/30) as Integer) / 2
|
||||
ret += t + " hr" + ((t > 1)? 's' : '')
|
||||
} else
|
||||
{
|
||||
t = (Math.round((359 + mins)/720) as Integer) / 2
|
||||
ret += t + " day" + ((t > 1)? 's' : '')
|
||||
}
|
||||
ret += (past)? " ago": ""
|
||||
|
||||
log.trace "ret: ${ret}"
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
def convertToTime(data)
|
||||
{
|
||||
def time = Integer.parseInt("$data", 16) as long;
|
||||
time *= 1000;
|
||||
time += 946684800000; // 481418694
|
||||
time -= location.timeZone.getRawOffset() + location.timeZone.getDSTSavings();
|
||||
|
||||
def d = new Date(time);
|
||||
|
||||
//log.trace "converted $data to Time $d"
|
||||
return d;
|
||||
}
|
||||
|
||||
def Program()
|
||||
{
|
||||
def currentSched = device.currentState("prorgammingOperation")?.value
|
||||
|
||||
def next = Integer.parseInt(currentSched?: "00", 16);
|
||||
if ( (next & 0x01) == 0x01)
|
||||
next = next & 0xfe;
|
||||
else
|
||||
next = next | 0x01;
|
||||
|
||||
def nextSched = getProgrammingMap()[next & 0x01]
|
||||
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x25 0x18 {$next}"
|
||||
|
||||
}
|
||||
|
||||
|
||||
def getThermostatOperatingState(value)
|
||||
{
|
||||
String[] m = [ "heating", "cooling", "fan", "Heat2", "Cool2", "Fan2", "Fan3"]
|
||||
String desc = 'idle'
|
||||
value = Integer.parseInt(''+value, 16)
|
||||
|
||||
// only check for 1-stage for A1730
|
||||
for ( i in 0..2 ) {
|
||||
if (value & 1 << i)
|
||||
desc = m[i]
|
||||
}
|
||||
|
||||
desc
|
||||
}
|
||||
|
||||
def checkLastTimeSync(delay)
|
||||
{
|
||||
def lastSync = device.currentState("lastTimeSync")?.value
|
||||
if (!lastSync)
|
||||
lastSync = "${new Date(0)}"
|
||||
|
||||
if (settings.sync_clock ?: false && lastSync != new Date(0))
|
||||
{
|
||||
sendEvent("name":"lastTimeSync", "value":"${new Date(0)}")
|
||||
}
|
||||
|
||||
|
||||
|
||||
long duration = (new Date()).getTime() - (new Date(lastSync)).getTime()
|
||||
|
||||
// log.debug "check Time: $lastSync duration: ${duration} settings.sync_clock: ${settings.sync_clock}"
|
||||
if (duration > 86400000)
|
||||
{
|
||||
sendEvent("name":"lastTimeSync", "value":"${new Date()}")
|
||||
return setThermostatTime()
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
def readAttributesCommand(cluster, attribList)
|
||||
{
|
||||
def attrString = ''
|
||||
|
||||
for ( val in attribList ) {
|
||||
attrString += ' ' + String.format("%02X %02X", val & 0xff , (val >> 8) & 0xff)
|
||||
}
|
||||
|
||||
//log.trace "list: " + attrString
|
||||
|
||||
["raw "+ cluster + " {00 00 00 $attrString}","delay 100",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 100",
|
||||
]
|
||||
}
|
||||
|
||||
def refresh()
|
||||
{
|
||||
log.debug "refresh called"
|
||||
// log.trace "list: " + readAttributesCommand(0x201, [0x1C,0x1E,0x23])
|
||||
|
||||
readAttributesCommand(0x201, [0x00,0x11,0x12]) +
|
||||
readAttributesCommand(0x201, [0x1C,0x1E,0x23]) +
|
||||
readAttributesCommand(0x201, [0x24,0x25,0x29]) +
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x204 0x01", "delay 200", // lock status
|
||||
"raw 0x201 {04 21 11 00 00 05 00 }" , "delay 500", // hold expiary
|
||||
"send 0x${device.deviceNetworkId} 1 1" , "delay 1500",
|
||||
] + checkLastTimeSync(2000)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
def poll() {
|
||||
log.trace "poll called"
|
||||
refresh()
|
||||
}
|
||||
|
||||
def getTemperature(value) {
|
||||
def celsius = Integer.parseInt("$value", 16) / 100
|
||||
|
||||
if(getTemperatureScale() == "C"){
|
||||
return celsius as Integer
|
||||
} else {
|
||||
return celsiusToFahrenheit(celsius) as Integer
|
||||
}
|
||||
}
|
||||
|
||||
def setHeatingSetpoint(degrees) {
|
||||
def temperatureScale = getTemperatureScale()
|
||||
|
||||
def degreesInteger = degrees as Integer
|
||||
sendEvent("name":"heatingSetpoint", "value":degreesInteger)
|
||||
|
||||
def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius*100) + "}"
|
||||
|
||||
}
|
||||
|
||||
def setCoolingSetpoint(degrees) {
|
||||
def degreesInteger = degrees as Integer
|
||||
sendEvent("name":"coolingSetpoint", "value":degreesInteger)
|
||||
def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2)
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius*100) + "}"
|
||||
|
||||
}
|
||||
|
||||
def modes() {
|
||||
["off", "heat", "cool"]
|
||||
}
|
||||
|
||||
def setThermostatFanMode() {
|
||||
def currentFanMode = device.currentState("thermostatFanMode")?.value
|
||||
//log.debug "switching fan from current mode: $currentFanMode"
|
||||
def returnCommand
|
||||
|
||||
switch (currentFanMode) {
|
||||
case "fanAuto":
|
||||
returnCommand = fanOn()
|
||||
break
|
||||
case "fanOn":
|
||||
returnCommand = fanAuto()
|
||||
break
|
||||
}
|
||||
if(!currentFanMode) { returnCommand = fanAuto() }
|
||||
returnCommand
|
||||
}
|
||||
|
||||
def setThermostatMode() {
|
||||
def currentMode = device.currentState("thermostatMode")?.value
|
||||
def modeOrder = modes()
|
||||
def index = modeOrder.indexOf(currentMode)
|
||||
def next = index >= 0 && index < modeOrder.size() - 1 ? modeOrder[index + 1] : modeOrder[0]
|
||||
|
||||
setThermostatMode(next)
|
||||
}
|
||||
|
||||
def setThermostatMode(String next) {
|
||||
def val = (getModeMap().find { it.value == next }?.key)?: "00"
|
||||
|
||||
// log.trace "mode changing to $next sending value: $val"
|
||||
|
||||
sendEvent("name":"thermostatMode", "value":"$next")
|
||||
["st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {$val}"] +
|
||||
refresh()
|
||||
}
|
||||
|
||||
def setThermostatFanMode(String value) {
|
||||
log.debug "setThermostatFanMode({$value})"
|
||||
"$value"()
|
||||
}
|
||||
|
||||
def off() {
|
||||
setThermostatMode("off")
|
||||
}
|
||||
|
||||
def cool() {
|
||||
setThermostatMode("cool")}
|
||||
|
||||
def heat() {
|
||||
setThermostatMode("heat")
|
||||
}
|
||||
|
||||
def auto() {
|
||||
setThermostatMode("auto")
|
||||
}
|
||||
|
||||
def on() {
|
||||
fanOn()
|
||||
}
|
||||
|
||||
def fanOn() {
|
||||
sendEvent("name":"thermostatFanMode", "value":"fanOn")
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {04}"
|
||||
}
|
||||
|
||||
|
||||
def fanAuto() {
|
||||
sendEvent("name":"thermostatFanMode", "value":"fanAuto")
|
||||
"st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {05}"
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
def lastSync = device.currentState("lastTimeSync")?.value
|
||||
if ((settings.sync_clock ?: false) == false)
|
||||
{
|
||||
log.debug "resetting last sync time. Used to be: $lastSync"
|
||||
sendEvent("name":"lastTimeSync", "value":"${new Date(0)}")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def getLockMap()
|
||||
{[
|
||||
"00":"Unlocked",
|
||||
"01":"Mode Only",
|
||||
"02":"Setpoint",
|
||||
"03":"Full",
|
||||
"04":"Full",
|
||||
"05":"Full",
|
||||
|
||||
]}
|
||||
def lock()
|
||||
{
|
||||
|
||||
def currentLock = device.currentState("lockLevel")?.value
|
||||
def val = getLockMap().find { it.value == currentLock }?.key
|
||||
|
||||
|
||||
|
||||
//log.debug "current lock is: ${val}"
|
||||
|
||||
if (val == "00")
|
||||
val = getLockMap().find { it.value == (settings.lock_level ?: "Full") }?.key
|
||||
else
|
||||
val = "00"
|
||||
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x204 0x01"
|
||||
|
||||
}
|
||||
|
||||
|
||||
def setThermostatTime()
|
||||
{
|
||||
|
||||
if ((settings.sync_clock ?: false))
|
||||
{
|
||||
log.debug "sync time is disabled, leaving"
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
Date date = new Date();
|
||||
String zone = location.timeZone.getRawOffset() + " DST " + location.timeZone.getDSTSavings();
|
||||
|
||||
long millis = date.getTime(); // Millis since Unix epoch
|
||||
millis -= 946684800000; // adjust for ZigBee EPOCH
|
||||
// adjust for time zone and DST offset
|
||||
millis += location.timeZone.getRawOffset() + location.timeZone.getDSTSavings();
|
||||
//convert to seconds
|
||||
millis /= 1000;
|
||||
|
||||
// print to a string for hex capture
|
||||
String s = String.format("%08X", millis);
|
||||
// hex capture for message format
|
||||
String data = " " + s.substring(6, 8) + " " + s.substring(4, 6) + " " + s.substring(2, 4)+ " " + s.substring(0, 2);
|
||||
|
||||
[
|
||||
"raw 0x201 {04 21 11 00 02 0f 00 23 ${data} }",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}"
|
||||
]
|
||||
}
|
||||
|
||||
def configure() {
|
||||
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x201 {${device.zigbeeId}} {}", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0000 0x29 20 300 {19 00}", // report temperature changes over 0.2C
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x001C 0x30 10 305 { }", // mode
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0025 0x18 10 310 { 00 }", // schedule on/off
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x001E 0x30 10 315 { 00 }", // running mode
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0011 0x29 10 320 {32 00}", // cooling setpoint delta: 0.5C (0x3200 in little endian)
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0012 0x29 10 320 {32 00}", // cooling setpoint delta: 0.5C (0x3200 in little endian)
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0029 0x19 10 325 { 00 }", "delay 200", // relay status
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x201 0x0023 0x30 10 330 { 00 }", // hold
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 1500",
|
||||
|
||||
] + refresh()
|
||||
}
|
||||
|
||||
private hex(value) {
|
||||
new BigInteger(Math.round(value).toString()).toString(16)
|
||||
}
|
||||
|
||||
private getEndpointId()
|
||||
{
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
}
|
||||
@@ -19,7 +19,6 @@ metadata {
|
||||
capability "Sensor"
|
||||
|
||||
fingerprint deviceId: "0x1000", inClusters: "0x25,0x72,0x86,0x71,0x22,0x70"
|
||||
fingerprint deviceId: "0x1006", inClusters: "0x25"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
|
||||
@@ -50,7 +50,7 @@ metadata {
|
||||
capability "Switch Level"
|
||||
capability "Polling"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019", manufacturer: "GE_Appliances", model: "ZLL Light", deviceJoinName: "GE Link Bulb"
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
@@ -317,7 +317,7 @@ def setLevel(value) {
|
||||
state.trigger = "setLevel"
|
||||
state.lvl = "${level}"
|
||||
|
||||
if (dimRate) {
|
||||
if (dimRate && (state?.rate != null)) {
|
||||
cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} ${state.rate}}"
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -26,8 +26,6 @@ metadata {
|
||||
capability "Actuator"
|
||||
capability "Sensor"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45852"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45857"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
|
||||
@@ -26,8 +26,6 @@ metadata {
|
||||
capability "Actuator"
|
||||
capability "Sensor"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "0003, 000A,0019", manufacturer: "Jasco Products", model: "45853"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45856"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 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: "Gentle Wake Up Controller", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Switch"
|
||||
capability "Timed Session"
|
||||
|
||||
attribute "percentComplete", "number"
|
||||
|
||||
command "setPercentComplete", ["number"]
|
||||
}
|
||||
|
||||
simulator {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
|
||||
multiAttributeTile(name: "richTile", type:"generic", width:6, height:4) {
|
||||
tileAttribute("sessionStatus", key: "PRIMARY_CONTROL") {
|
||||
attributeState "cancelled", action: "timed session.start", icon: "http://f.cl.ly/items/322n181j2K3f281r2s0A/playbutton.png", backgroundColor: "#ffffff", nextState: "running"
|
||||
attributeState "stopped", action: "timed session.start", icon: "http://f.cl.ly/items/322n181j2K3f281r2s0A/playbutton.png", backgroundColor: "#ffffff", nextState: "cancelled"
|
||||
attributeState "running", action: "timed session.stop", icon: "http://f.cl.ly/items/0B3y3p2V3X2l3P3y3W09/stopbutton.png", backgroundColor: "#79b821", nextState: "cancelled"
|
||||
}
|
||||
tileAttribute("timeRemaining", key: "SECONDARY_CONTROL") {
|
||||
attributeState "timeRemaining", label:'${currentValue} remaining'
|
||||
}
|
||||
tileAttribute("percentComplete", key: "SLIDER_CONTROL") {
|
||||
attributeState "percentComplete", action: "timed session.setTimeRemaining"
|
||||
}
|
||||
}
|
||||
|
||||
// start/stop
|
||||
standardTile("sessionStatusTile", "sessionStatus", width: 1, height: 1, canChangeIcon: true) {
|
||||
state "cancelled", label: "Stopped", action: "timed session.start", backgroundColor: "#ffffff", icon: "http://f.cl.ly/items/1J1g0H2P0S1G1f2O1s1s/icon.png"
|
||||
state "stopped", label: "Stopped", action: "timed session.start", backgroundColor: "#ffffff", icon: "http://f.cl.ly/items/1J1g0H2P0S1G1f2O1s1s/icon.png"
|
||||
state "running", label: "Running", action: "timed session.stop", backgroundColor: "#79b821", icon: "http://f.cl.ly/items/1J1g0H2P0S1G1f2O1s1s/icon.png"
|
||||
}
|
||||
|
||||
// duration
|
||||
valueTile("timeRemainingTile", "timeRemaining", decoration: "flat", width: 2) {
|
||||
state "timeRemaining", label:'${currentValue} left'
|
||||
}
|
||||
controlTile("percentCompleteTile", "percentComplete", "slider", height: 1, width: 3) {
|
||||
state "percentComplete", action: "timed session.setTimeRemaining"
|
||||
}
|
||||
|
||||
main "sessionStatusTile"
|
||||
details "richTile"
|
||||
// details(["richTile", "sessionStatusTile", "timeRemainingTile", "percentCompleteTile"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
// TODO: handle 'switch' attribute
|
||||
// TODO: handle 'level' attribute
|
||||
// TODO: handle 'sessionStatus' attribute
|
||||
// TODO: handle 'timeRemaining' attribute
|
||||
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def on() {
|
||||
log.debug "Executing 'on'"
|
||||
startDimming()
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "Executing 'off'"
|
||||
stopDimming()
|
||||
}
|
||||
|
||||
def setTimeRemaining(percentComplete) {
|
||||
log.debug "Executing 'setTimeRemaining' to ${percentComplete}% complete"
|
||||
parent.jumpTo(percentComplete)
|
||||
}
|
||||
|
||||
def start() {
|
||||
log.debug "Executing 'start'"
|
||||
startDimming()
|
||||
}
|
||||
|
||||
def stop() {
|
||||
log.debug "Executing 'stop'"
|
||||
stopDimming()
|
||||
}
|
||||
|
||||
def pause() {
|
||||
log.debug "Executing 'pause'"
|
||||
// TODO: handle 'pause' command
|
||||
}
|
||||
|
||||
def cancel() {
|
||||
log.debug "Executing 'cancel'"
|
||||
stopDimming()
|
||||
}
|
||||
|
||||
def startDimming() {
|
||||
log.trace "startDimming"
|
||||
log.debug "parent: ${parent}"
|
||||
parent.start("controller")
|
||||
}
|
||||
|
||||
def stopDimming() {
|
||||
log.trace "stopDimming"
|
||||
log.debug "parent: ${parent}"
|
||||
parent.stop("controller")
|
||||
}
|
||||
|
||||
def controllerEvent(eventData) {
|
||||
log.trace "controllerEvent"
|
||||
sendEvent(eventData)
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Logitech Harmony Activity
|
||||
*
|
||||
* Copyright 2015 Juan Risso
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Harmony Activity", namespace: "smartthings", author: "Juan Risso") {
|
||||
capability "Switch"
|
||||
capability "Actuator"
|
||||
capability "Refresh"
|
||||
|
||||
command "huboff"
|
||||
command "alloff"
|
||||
command "refresh"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles {
|
||||
standardTile("button", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "off", label: 'Off', action: "switch.on", icon: "st.harmony.harmony-hub-icon", backgroundColor: "#ffffff", nextState: "on"
|
||||
state "on", label: 'On', action: "switch.off", icon: "st.harmony.harmony-hub-icon", backgroundColor: "#79b821", nextState: "off"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
standardTile("forceoff", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'Force End', action:"switch.off", icon:"st.secondary.off"
|
||||
}
|
||||
standardTile("huboff", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'End Hub Action', action:"huboff", icon:"st.harmony.harmony-hub-icon"
|
||||
}
|
||||
standardTile("alloff", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'All Actions', action:"alloff", icon:"st.secondary.off"
|
||||
}
|
||||
main "button"
|
||||
details(["button", "refresh", "forceoff", "huboff", "alloff"])
|
||||
}
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
}
|
||||
|
||||
def on() {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
log.trace parent.activity(device.deviceNetworkId,"start")
|
||||
}
|
||||
|
||||
def off() {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
log.trace parent.activity(device.deviceNetworkId,"end")
|
||||
}
|
||||
|
||||
def huboff() {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
log.trace parent.activity(device.deviceNetworkId,"hub")
|
||||
}
|
||||
|
||||
def alloff() {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
log.trace parent.activity("all","end")
|
||||
}
|
||||
|
||||
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
log.trace parent.poll()
|
||||
}
|
||||
@@ -15,19 +15,27 @@ metadata {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
tiles {
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"rich-control"){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#F3C200"
|
||||
}
|
||||
tileAttribute ("serialNumber", key: "SECONDARY_CONTROL") {
|
||||
attributeState "default", label:'SN: ${currentValue}'
|
||||
}
|
||||
}
|
||||
standardTile("icon", "icon", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) {
|
||||
state "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#FFFFFF"
|
||||
}
|
||||
valueTile("serialNumber", "device.serialNumber", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
|
||||
state "default", label:'SN: ${currentValue}'
|
||||
}
|
||||
valueTile("networkAddress", "device.networkAddress", decoration: "flat", height: 1, width: 2, inactiveLabel: false) {
|
||||
valueTile("networkAddress", "device.networkAddress", decoration: "flat", height: 2, width: 4, inactiveLabel: false) {
|
||||
state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false
|
||||
}
|
||||
|
||||
main (["icon"])
|
||||
details(["networkAddress","serialNumber"])
|
||||
details(["rich-control", "networkAddress"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +44,6 @@ def parse(description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
def results = []
|
||||
def result = parent.parse(this, description)
|
||||
|
||||
if (result instanceof physicalgraph.device.HubAction){
|
||||
log.trace "HUE BRIDGE HubAction received -- DOES THIS EVER HAPPEN?"
|
||||
results << result
|
||||
@@ -44,32 +51,30 @@ def parse(description) {
|
||||
//do nothing
|
||||
log.trace "HUE BRIDGE was updated"
|
||||
} else {
|
||||
log.trace "HUE BRIDGE, OTHER"
|
||||
def map = description
|
||||
if (description instanceof String) {
|
||||
map = stringToMap(description)
|
||||
}
|
||||
if (map?.name && map?.value) {
|
||||
log.trace "HUE BRIDGE, GENERATING EVENT: $map.name: $map.value"
|
||||
results << createEvent(name: "${map?.name}", value: "${map?.value}")
|
||||
}
|
||||
else {
|
||||
log.trace "HUE BRIDGE, OTHER"
|
||||
results << createEvent(name: "${map.name}", value: "${map.value}")
|
||||
} else {
|
||||
log.trace "Parsing description"
|
||||
def msg = parseLanMessage(description)
|
||||
if (msg.body) {
|
||||
def contentType = msg.headers["Content-Type"]
|
||||
if (contentType?.contains("json")) {
|
||||
def bulbs = new groovy.json.JsonSlurper().parseText(msg.body)
|
||||
if (bulbs.state) {
|
||||
log.warn "NOT PROCESSED: $msg.body"
|
||||
}
|
||||
else {
|
||||
log.debug "HUE BRIDGE, GENERATING BULB LIST EVENT: $bulbs"
|
||||
sendEvent(name: "bulbList", value: device.hub.id, isStateChange: true, data: bulbs, displayed: false)
|
||||
log.info "Bridge response: $msg.body"
|
||||
} else {
|
||||
// Sending Bulbs List to parent"
|
||||
if (parent.state.inBulbDiscovery)
|
||||
log.info parent.bulbListHandler(device.hub.id, msg.body)
|
||||
}
|
||||
}
|
||||
else if (contentType?.contains("xml")) {
|
||||
log.debug "HUE BRIDGE, SWALLOWING BRIDGE DESCRIPTION RESPONSE -- BRIDGE ALREADY PRESENT"
|
||||
log.debug "HUE BRIDGE ALREADY PRESENT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
/**
|
||||
* Hue Bulb
|
||||
*
|
||||
@@ -15,8 +16,8 @@ metadata {
|
||||
capability "Sensor"
|
||||
|
||||
command "setAdjustedColor"
|
||||
command "reset"
|
||||
command "refresh"
|
||||
command "reset"
|
||||
command "refresh"
|
||||
}
|
||||
|
||||
simulator {
|
||||
@@ -49,7 +50,6 @@ metadata {
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "levelSliderControl", "rgbSelector", "refresh", "reset"])
|
||||
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
@@ -68,13 +68,13 @@ def parse(description) {
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def on(transition = "4") {
|
||||
log.trace parent.on(this,transition)
|
||||
def on() {
|
||||
log.trace parent.on(this)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
|
||||
def off(transition = "4") {
|
||||
log.trace parent.off(this,transition)
|
||||
def off() {
|
||||
log.trace parent.off(this)
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
|
||||
@@ -107,9 +107,9 @@ def setHue(percent) {
|
||||
sendEvent(name: "hue", value: percent)
|
||||
}
|
||||
|
||||
def setColor(value,alert = "none",transition = 4) {
|
||||
def setColor(value) {
|
||||
log.debug "setColor: ${value}, $this"
|
||||
parent.setColor(this, value, alert, transition)
|
||||
parent.setColor(this, value)
|
||||
if (value.hue) { sendEvent(name: "hue", value: value.hue)}
|
||||
if (value.saturation) { sendEvent(name: "saturation", value: value.saturation)}
|
||||
if (value.hex) { sendEvent(name: "color", value: value.hex)}
|
||||
|
||||
@@ -19,24 +19,41 @@ metadata {
|
||||
simulator {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"rich-control", type: "lighting", canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel", range:"(0..100)"
|
||||
}
|
||||
tileAttribute ("device.level", key: "SECONDARY_CONTROL") {
|
||||
attributeState "level", label: 'Level ${currentValue}%'
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
state "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
|
||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821"
|
||||
state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
|
||||
state "level", action:"switch level.setLevel"
|
||||
}
|
||||
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
|
||||
state "level", label: 'Level ${currentValue}%'
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "levelSliderControl", "refresh"])
|
||||
|
||||
main(["switch"])
|
||||
details(["rich-control", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* LIFX Color Bulb
|
||||
*
|
||||
* Copyright 2015 LIFX
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "LIFX Color Bulb", namespace: "smartthings", author: "LIFX") {
|
||||
capability "Actuator"
|
||||
capability "Color Control"
|
||||
capability "Color Temperature"
|
||||
capability "Switch"
|
||||
capability "Switch Level" // brightness
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
}
|
||||
|
||||
simulator {
|
||||
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
tileAttribute ("device.color", key: "COLOR_CONTROL") {
|
||||
attributeState "color", action:"setColor"
|
||||
}
|
||||
|
||||
tileAttribute ("device.model", key: "SECONDARY_CONTROL") {
|
||||
attributeState "model", label: '${currentValue}'
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:''
|
||||
}
|
||||
|
||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 2, width: 4, inactiveLabel: false, range:"(2700..9000)") {
|
||||
state "colorTemp", action:"color temperature.setColorTemperature"
|
||||
}
|
||||
|
||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", height: 2, width: 2) {
|
||||
state "colorTemp", label: '${currentValue}K'
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def setHue(percentage) {
|
||||
log.debug "setHue ${percentage}"
|
||||
parent.logErrors(logObject: log) {
|
||||
def resp = parent.apiPUT("/lights/${selector()}/state", [color: "hue:${percentage * 3.6}", power: "on"])
|
||||
if (resp.status < 300) {
|
||||
sendEvent(name: "hue", value: percentage)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.error("Bad setHue result: [${resp.status}] ${resp.data}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def setSaturation(percentage) {
|
||||
log.debug "setSaturation ${percentage}"
|
||||
parent.logErrors(logObject: log) {
|
||||
def resp = parent.apiPUT("/lights/${selector()}/state", [color: "saturation:${percentage / 100}", power: "on"])
|
||||
if (resp.status < 300) {
|
||||
sendEvent(name: "saturation", value: percentage)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.error("Bad setSaturation result: [${resp.status}] ${resp.data}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def setColor(Map color) {
|
||||
log.debug "setColor ${color}"
|
||||
def attrs = []
|
||||
def events = []
|
||||
color.each { key, value ->
|
||||
switch (key) {
|
||||
case "hue":
|
||||
attrs << "hue:${value * 3.6}"
|
||||
events << createEvent(name: "hue", value: value)
|
||||
break
|
||||
case "saturation":
|
||||
attrs << "saturation:${value / 100}"
|
||||
events << createEvent(name: "saturation", value: value)
|
||||
break
|
||||
case "colorTemperature":
|
||||
attrs << "kelvin:${value}"
|
||||
events << createEvent(name: "colorTemperature", value: value)
|
||||
break
|
||||
}
|
||||
}
|
||||
parent.logErrors(logObject:log) {
|
||||
def resp = parent.apiPUT("/lights/${selector()}/state", [color: attrs.join(" "), power: "on"])
|
||||
if (resp.status < 300) {
|
||||
sendEvent(name: "color", value: color.hex)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
events.each { sendEvent(it) }
|
||||
} else {
|
||||
log.error("Bad setColor result: [${resp.status}] ${resp.data}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def setLevel(percentage) {
|
||||
log.debug "setLevel ${percentage}"
|
||||
if (percentage < 1 && percentage > 0) {
|
||||
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) {
|
||||
def resp = parent.apiPUT("/lights/${selector()}/state", ["brightness": percentage / 100, "power": "on"])
|
||||
if (resp.status < 300) {
|
||||
sendEvent(name: "level", value: percentage)
|
||||
sendEvent(name: "switch.setLevel", value: percentage)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def setColorTemperature(kelvin) {
|
||||
log.debug "Executing 'setColorTemperature' to ${kelvin}"
|
||||
parent.logErrors() {
|
||||
def resp = parent.apiPUT("/lights/${selector()}/state", [color: "kelvin:${kelvin}", power: "on"])
|
||||
if (resp.status < 300) {
|
||||
sendEvent(name: "colorTemperature", value: kelvin)
|
||||
sendEvent(name: "color", value: "#ffffff")
|
||||
sendEvent(name: "saturation", value: 0)
|
||||
} else {
|
||||
log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
def on() {
|
||||
log.debug "Device setOn"
|
||||
parent.logErrors() {
|
||||
if (parent.apiPUT("/lights/${selector()}/state", [power: "on"]) != null) {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "Device setOff"
|
||||
parent.logErrors() {
|
||||
if (parent.apiPUT("/lights/${selector()}/state", [power: "off"]) != null) {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def poll() {
|
||||
log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}"
|
||||
def resp = parent.apiGET("/lights/${selector()}")
|
||||
if (resp.status == 404) {
|
||||
sendEvent(name: "switch", value: "unreachable")
|
||||
return []
|
||||
} else if (resp.status != 200) {
|
||||
log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}")
|
||||
return []
|
||||
}
|
||||
def data = resp.data[0]
|
||||
log.debug("Data: ${data}")
|
||||
|
||||
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: "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}")
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
poll()
|
||||
}
|
||||
|
||||
def selector() {
|
||||
if (device.deviceNetworkId.contains(":")) {
|
||||
return device.deviceNetworkId
|
||||
} else {
|
||||
return "id:${device.deviceNetworkId}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* LIFX White Bulb
|
||||
*
|
||||
* Copyright 2015 LIFX
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "LIFX White Bulb", namespace: "smartthings", author: "LIFX") {
|
||||
capability "Actuator"
|
||||
capability "Color Temperature"
|
||||
capability "Switch"
|
||||
capability "Switch Level" // brightness
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
}
|
||||
|
||||
simulator {
|
||||
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:''
|
||||
}
|
||||
|
||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 2, width: 4, inactiveLabel: false, range:"(2700..9000)") {
|
||||
state "colorTemp", action:"color temperature.setColorTemperature"
|
||||
}
|
||||
|
||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", height: 2, width: 2) {
|
||||
state "colorTemp", label: '${currentValue}K'
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def setLevel(percentage) {
|
||||
log.debug "setLevel ${percentage}"
|
||||
if (percentage < 1 && percentage > 0) {
|
||||
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) {
|
||||
def resp = parent.apiPUT("/lights/${selector()}/state", [brightness: percentage / 100, power: "on"])
|
||||
if (resp.status < 300) {
|
||||
sendEvent(name: "level", value: percentage)
|
||||
sendEvent(name: "switch.setLevel", value: percentage)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.error("Bad setLevel result: [${resp.status}] ${resp.data}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def setColorTemperature(kelvin) {
|
||||
log.debug "Executing 'setColorTemperature' to ${kelvin}"
|
||||
parent.logErrors() {
|
||||
def resp = parent.apiPUT("/lights/${selector()}/state", [color: "kelvin:${kelvin}", power: "on"])
|
||||
if (resp.status < 300) {
|
||||
sendEvent(name: "colorTemperature", value: kelvin)
|
||||
sendEvent(name: "color", value: "#ffffff")
|
||||
sendEvent(name: "saturation", value: 0)
|
||||
sendEvent(name: "switch", value: "on")
|
||||
} else {
|
||||
log.error("Bad setColorTemperature result: [${resp.status}] ${resp.data}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def on() {
|
||||
log.debug "Device setOn"
|
||||
parent.logErrors() {
|
||||
if (parent.apiPUT("/lights/${selector()}/state", [power: "on"]) != null) {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "Device setOff"
|
||||
parent.logErrors() {
|
||||
if (parent.apiPUT("/lights/${selector()}/state", [power: "off"]) != null) {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def poll() {
|
||||
log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}"
|
||||
def resp = parent.apiGET("/lights/${selector()}")
|
||||
if (resp.status == 404) {
|
||||
sendEvent(name: "switch", value: "unreachable")
|
||||
return []
|
||||
} else if (resp.status != 200) {
|
||||
log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}")
|
||||
return []
|
||||
}
|
||||
def data = resp.data[0]
|
||||
|
||||
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: "colorTemperature", value: data.color.kelvin)
|
||||
sendEvent(name: "model", value: data.product.name)
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
poll()
|
||||
}
|
||||
|
||||
def selector() {
|
||||
if (device.deviceNetworkId.contains(":")) {
|
||||
return device.deviceNetworkId
|
||||
} else {
|
||||
return "id:${device.deviceNetworkId}"
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ metadata {
|
||||
tiles {
|
||||
standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) {
|
||||
state("present", labelIcon:"st.presence.tile.mobile-present", backgroundColor:"#53a7c0")
|
||||
state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#ffffff")
|
||||
state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#ebeef2")
|
||||
}
|
||||
main "presence"
|
||||
details "presence"
|
||||
|
||||
@@ -24,8 +24,8 @@ metadata {
|
||||
command "enrollResponse"
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3041"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3043"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3045"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3043", deviceJoinName: "NYCE Ceiling Motion Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3045", deviceJoinName: "NYCE Curtain Motion Sensor"
|
||||
}
|
||||
|
||||
tiles {
|
||||
|
||||
@@ -24,10 +24,10 @@ metadata {
|
||||
command "enrollResponse"
|
||||
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011"
|
||||
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014"
|
||||
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3014"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor"
|
||||
}
|
||||
|
||||
simulator {
|
||||
|
||||
@@ -22,10 +22,8 @@ metadata {
|
||||
|
||||
command "setAdjustedColor"
|
||||
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenspot RGB"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Gardenspot RGB"
|
||||
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
Osram bulbs have a firmware issue causing it to forget its dimming level when turned off (via commands). Handling
|
||||
that issue by using state variables
|
||||
*/
|
||||
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
|
||||
|
||||
metadata {
|
||||
definition (name: "OSRAM LIGHTIFY LED Flexible Strip RGBW", namespace: "smartthings", author: "SmartThings") {
|
||||
@@ -23,8 +24,8 @@ metadata {
|
||||
command "setAdjustedColor"
|
||||
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Flex RGBW"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW"
|
||||
//fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Flex RGBW"
|
||||
//fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW"
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
that issue by using state variables
|
||||
*/
|
||||
|
||||
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
|
||||
|
||||
metadata {
|
||||
definition (name: "OSRAM LIGHTIFY LED Tunable White 60W", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
@@ -20,10 +22,7 @@ metadata {
|
||||
|
||||
// indicates that device keeps track of heartbeat (in state.heartbeat)
|
||||
attribute "heartbeat", "string"
|
||||
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 TW"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 Tunable White"
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ metadata {
|
||||
capability "Switch"
|
||||
capability "Sensor"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0006", outClusters: "0019"
|
||||
fingerprint profileId: "0104", inClusters: "0006, 0004, 0003, 0000, 0005", outClusters: "0019", manufacturer: "Compacta International, Ltd", model: "ZBMPlug15", deviceJoinName: "SmartPower Outlet V1"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
|
||||
@@ -28,6 +28,9 @@ metadata {
|
||||
// indicates that device keeps track of heartbeat (in state.heartbeat)
|
||||
attribute "heartbeat", "string"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200", deviceJoinName: "Outlet"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200-Sgb", deviceJoinName: "Outlet"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "4257050-RZHAC", deviceJoinName: "Outlet"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019"
|
||||
}
|
||||
|
||||
@@ -45,8 +48,8 @@ metadata {
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS2.png"
|
||||
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS1.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS2.jpg"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,8 +79,8 @@ metadata {
|
||||
}
|
||||
|
||||
preferences {
|
||||
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,8 +88,8 @@ metadata {
|
||||
}
|
||||
|
||||
preferences {
|
||||
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,18 +20,18 @@ metadata {
|
||||
capability "Refresh"
|
||||
capability "Temperature Measurement"
|
||||
capability "Water Sensor"
|
||||
|
||||
command "enrollResponse"
|
||||
|
||||
command "enrollResponse"
|
||||
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-S"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-S", deviceJoinName: "Water Leak Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-Seu"
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-Seu", deviceJoinName: "Water Leak Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "moisturev4", deviceJoinName: "Water Leak Sensor"
|
||||
}
|
||||
|
||||
|
||||
simulator {
|
||||
|
||||
|
||||
}
|
||||
|
||||
preferences {
|
||||
@@ -43,11 +43,11 @@ metadata {
|
||||
])
|
||||
}
|
||||
section {
|
||||
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
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:"water", type: "generic", width: 6, height: 4){
|
||||
tileAttribute ("device.water", key: "PRIMARY_CONTROL") {
|
||||
@@ -78,7 +78,7 @@ metadata {
|
||||
details(["water", "temperature", "battery", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def parse(String description) {
|
||||
log.debug "description: $description"
|
||||
|
||||
@@ -92,59 +92,59 @@ def parse(String description) {
|
||||
else if (description?.startsWith('temperature: ')) {
|
||||
map = parseCustomMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('zone status')) {
|
||||
map = parseIasMessage(description)
|
||||
}
|
||||
|
||||
else if (description?.startsWith('zone status')) {
|
||||
map = parseIasMessage(description)
|
||||
}
|
||||
|
||||
log.debug "Parse returned $map"
|
||||
def result = map ? createEvent(map) : null
|
||||
|
||||
if (description?.startsWith('enroll request')) {
|
||||
List cmds = enrollResponse()
|
||||
log.debug "enroll response: ${cmds}"
|
||||
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||
}
|
||||
return result
|
||||
|
||||
if (description?.startsWith('enroll request')) {
|
||||
List cmds = enrollResponse()
|
||||
log.debug "enroll response: ${cmds}"
|
||||
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0001:
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
break
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0001:
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
case 0x0402:
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return resultMap
|
||||
return resultMap
|
||||
}
|
||||
|
||||
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
|
||||
// 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 Map 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 = [:]
|
||||
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
||||
def value = getTemperature(descMap.value)
|
||||
@@ -153,10 +153,10 @@ private Map parseReportAttributeMessage(String description) {
|
||||
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
||||
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||
}
|
||||
|
||||
|
||||
return resultMap
|
||||
}
|
||||
|
||||
|
||||
private Map parseCustomMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
if (description?.startsWith('temperature: ')) {
|
||||
@@ -167,42 +167,42 @@ private Map parseCustomMessage(String description) {
|
||||
}
|
||||
|
||||
private Map parseIasMessage(String description) {
|
||||
List parsedMsg = description.split(' ')
|
||||
String msgCode = parsedMsg[2]
|
||||
|
||||
Map resultMap = [:]
|
||||
switch(msgCode) {
|
||||
case '0x0020': // Closed/No Motion/Dry
|
||||
resultMap = getMoistureResult('dry')
|
||||
break
|
||||
List parsedMsg = description.split(' ')
|
||||
String msgCode = parsedMsg[2]
|
||||
|
||||
case '0x0021': // Open/Motion/Wet
|
||||
resultMap = getMoistureResult('wet')
|
||||
break
|
||||
Map resultMap = [:]
|
||||
switch(msgCode) {
|
||||
case '0x0020': // Closed/No Motion/Dry
|
||||
resultMap = getMoistureResult('dry')
|
||||
break
|
||||
|
||||
case '0x0022': // Tamper Alarm
|
||||
break
|
||||
case '0x0021': // Open/Motion/Wet
|
||||
resultMap = getMoistureResult('wet')
|
||||
break
|
||||
|
||||
case '0x0023': // Battery Alarm
|
||||
break
|
||||
case '0x0022': // Tamper Alarm
|
||||
break
|
||||
|
||||
case '0x0024': // Supervision Report
|
||||
log.debug 'dry with tamper alarm'
|
||||
resultMap = getMoistureResult('dry')
|
||||
break
|
||||
case '0x0023': // Battery Alarm
|
||||
break
|
||||
|
||||
case '0x0025': // Restore Report
|
||||
log.debug 'water with tamper alarm'
|
||||
resultMap = getMoistureResult('wet')
|
||||
break
|
||||
case '0x0024': // Supervision Report
|
||||
log.debug 'dry with tamper alarm'
|
||||
resultMap = getMoistureResult('dry')
|
||||
break
|
||||
|
||||
case '0x0026': // Trouble/Failure
|
||||
break
|
||||
case '0x0025': // Restore Report
|
||||
log.debug 'water with tamper alarm'
|
||||
resultMap = getMoistureResult('wet')
|
||||
break
|
||||
|
||||
case '0x0028': // Test Mode
|
||||
break
|
||||
}
|
||||
return resultMap
|
||||
case '0x0026': // Trouble/Failure
|
||||
break
|
||||
|
||||
case '0x0028': // Test Mode
|
||||
break
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
def getTemperature(value) {
|
||||
@@ -215,24 +215,47 @@ def getTemperature(value) {
|
||||
}
|
||||
|
||||
private Map getBatteryResult(rawValue) {
|
||||
log.debug 'Battery'
|
||||
log.debug "Battery rawValue = ${rawValue}"
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
def result = [
|
||||
name: 'battery'
|
||||
]
|
||||
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: '--'
|
||||
]
|
||||
|
||||
def volts = rawValue / 10
|
||||
def descriptionText
|
||||
if (volts > 3.5) {
|
||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||
}
|
||||
|
||||
if (rawValue == 0 || rawValue == 255) {}
|
||||
else {
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
result.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
if (volts > 3.5) {
|
||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||
}
|
||||
else {
|
||||
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,
|
||||
22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
|
||||
def minVolts = 15
|
||||
def maxVolts = 28
|
||||
|
||||
if (volts < minVolts)
|
||||
volts = minVolts
|
||||
else if (volts > maxVolts)
|
||||
volts = maxVolts
|
||||
def pct = batteryMap[volts]
|
||||
if (pct != null) {
|
||||
result.value = pct
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
}
|
||||
else {
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
result.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -267,7 +290,7 @@ private Map getMoistureResult(value) {
|
||||
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 0x402 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200"
|
||||
]
|
||||
|
||||
@@ -277,34 +300,32 @@ def refresh() {
|
||||
def configure() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 1 0x20 0x20 300 0600 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x001 {${device.zigbeeId}} {}", "delay 500"
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 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
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
log.debug "Sending enroll response"
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
[
|
||||
[
|
||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
@@ -316,19 +337,19 @@ private hex(value) {
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
int i = 0;
|
||||
int j = array.length - 1;
|
||||
byte tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
return array
|
||||
int i = 0;
|
||||
int j = array.length - 1;
|
||||
byte tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
@@ -19,16 +19,17 @@ metadata {
|
||||
capability "Motion Sensor"
|
||||
capability "Configuration"
|
||||
capability "Battery"
|
||||
capability "Temperature Measurement"
|
||||
capability "Temperature Measurement"
|
||||
capability "Refresh"
|
||||
|
||||
command "enrollResponse"
|
||||
|
||||
command "enrollResponse"
|
||||
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305-S"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S", deviceJoinName: "Motion Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326"
|
||||
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "motionv4", deviceJoinName: "Motion Sensor"
|
||||
}
|
||||
|
||||
simulator {
|
||||
@@ -39,14 +40,14 @@ metadata {
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion2.png",
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion3.png"
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion1.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion2.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Motion/Motion3.jpg"
|
||||
])
|
||||
}
|
||||
section {
|
||||
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +85,7 @@ metadata {
|
||||
|
||||
def parse(String description) {
|
||||
log.debug "description: $description"
|
||||
|
||||
|
||||
Map map = [:]
|
||||
if (description?.startsWith('catchall:')) {
|
||||
map = parseCatchAllMessage(description)
|
||||
@@ -95,55 +96,55 @@ def parse(String description) {
|
||||
else if (description?.startsWith('temperature: ')) {
|
||||
map = parseCustomMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('zone status')) {
|
||||
map = parseIasMessage(description)
|
||||
}
|
||||
|
||||
else if (description?.startsWith('zone status')) {
|
||||
map = parseIasMessage(description)
|
||||
}
|
||||
|
||||
log.debug "Parse returned $map"
|
||||
def result = map ? createEvent(map) : null
|
||||
|
||||
if (description?.startsWith('enroll request')) {
|
||||
List cmds = enrollResponse()
|
||||
log.debug "enroll response: ${cmds}"
|
||||
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||
}
|
||||
return result
|
||||
|
||||
if (description?.startsWith('enroll request')) {
|
||||
List cmds = enrollResponse()
|
||||
log.debug "enroll response: ${cmds}"
|
||||
result = cmds?.collect { new physicalgraph.device.HubAction(it) }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0001:
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
break
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0001:
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
break
|
||||
case 0x0402:
|
||||
// temp is last 2 data values. reverse to swap endian
|
||||
String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join()
|
||||
def value = getTemperature(temp)
|
||||
resultMap = getTemperatureResult(value)
|
||||
break
|
||||
|
||||
case 0x0406:
|
||||
log.debug 'motion'
|
||||
resultMap.name = 'motion'
|
||||
break
|
||||
}
|
||||
}
|
||||
log.debug 'motion'
|
||||
resultMap.name = 'motion'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return resultMap
|
||||
return resultMap
|
||||
}
|
||||
|
||||
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
|
||||
// 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 Map parseReportAttributeMessage(String description) {
|
||||
@@ -152,7 +153,7 @@ private Map parseReportAttributeMessage(String description) {
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
log.debug "Desc Map: $descMap"
|
||||
|
||||
|
||||
Map resultMap = [:]
|
||||
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
|
||||
def value = getTemperature(descMap.value)
|
||||
@@ -161,14 +162,14 @@ private Map parseReportAttributeMessage(String description) {
|
||||
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
|
||||
resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16))
|
||||
}
|
||||
else if (descMap.cluster == "0406" && descMap.attrId == "0000") {
|
||||
def value = descMap.value.endsWith("01") ? "active" : "inactive"
|
||||
resultMap = getMotionResult(value)
|
||||
}
|
||||
|
||||
else if (descMap.cluster == "0406" && descMap.attrId == "0000") {
|
||||
def value = descMap.value.endsWith("01") ? "active" : "inactive"
|
||||
resultMap = getMotionResult(value)
|
||||
}
|
||||
|
||||
return resultMap
|
||||
}
|
||||
|
||||
|
||||
private Map parseCustomMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
if (description?.startsWith('temperature: ')) {
|
||||
@@ -179,44 +180,44 @@ private Map parseCustomMessage(String description) {
|
||||
}
|
||||
|
||||
private Map parseIasMessage(String description) {
|
||||
List parsedMsg = description.split(' ')
|
||||
String msgCode = parsedMsg[2]
|
||||
|
||||
Map resultMap = [:]
|
||||
switch(msgCode) {
|
||||
case '0x0020': // Closed/No Motion/Dry
|
||||
resultMap = getMotionResult('inactive')
|
||||
break
|
||||
List parsedMsg = description.split(' ')
|
||||
String msgCode = parsedMsg[2]
|
||||
|
||||
case '0x0021': // Open/Motion/Wet
|
||||
resultMap = getMotionResult('active')
|
||||
break
|
||||
Map resultMap = [:]
|
||||
switch(msgCode) {
|
||||
case '0x0020': // Closed/No Motion/Dry
|
||||
resultMap = getMotionResult('inactive')
|
||||
break
|
||||
|
||||
case '0x0022': // Tamper Alarm
|
||||
log.debug 'motion with tamper alarm'
|
||||
resultMap = getMotionResult('active')
|
||||
break
|
||||
case '0x0021': // Open/Motion/Wet
|
||||
resultMap = getMotionResult('active')
|
||||
break
|
||||
|
||||
case '0x0023': // Battery Alarm
|
||||
break
|
||||
case '0x0022': // Tamper Alarm
|
||||
log.debug 'motion with tamper alarm'
|
||||
resultMap = getMotionResult('active')
|
||||
break
|
||||
|
||||
case '0x0024': // Supervision Report
|
||||
log.debug 'no motion with tamper alarm'
|
||||
resultMap = getMotionResult('inactive')
|
||||
break
|
||||
case '0x0023': // Battery Alarm
|
||||
break
|
||||
|
||||
case '0x0025': // Restore Report
|
||||
break
|
||||
case '0x0024': // Supervision Report
|
||||
log.debug 'no motion with tamper alarm'
|
||||
resultMap = getMotionResult('inactive')
|
||||
break
|
||||
|
||||
case '0x0026': // Trouble/Failure
|
||||
log.debug 'motion with failure alarm'
|
||||
resultMap = getMotionResult('active')
|
||||
break
|
||||
case '0x0025': // Restore Report
|
||||
break
|
||||
|
||||
case '0x0028': // Test Mode
|
||||
break
|
||||
}
|
||||
return resultMap
|
||||
case '0x0026': // Trouble/Failure
|
||||
log.debug 'motion with failure alarm'
|
||||
resultMap = getMotionResult('active')
|
||||
break
|
||||
|
||||
case '0x0028': // Test Mode
|
||||
break
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
def getTemperature(value) {
|
||||
@@ -229,30 +230,46 @@ def getTemperature(value) {
|
||||
}
|
||||
|
||||
private Map getBatteryResult(rawValue) {
|
||||
log.debug 'Battery'
|
||||
log.debug "Battery rawValue = ${rawValue}"
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
log.debug rawValue
|
||||
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: '--'
|
||||
]
|
||||
|
||||
def volts = rawValue / 10
|
||||
def descriptionText
|
||||
|
||||
if (rawValue == 0) {}
|
||||
if (rawValue == 0 || rawValue == 255) {}
|
||||
else {
|
||||
if (volts > 3.5) {
|
||||
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
|
||||
}
|
||||
else if (volts > 0){
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
result.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
else {
|
||||
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,
|
||||
22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
|
||||
def minVolts = 15
|
||||
def maxVolts = 28
|
||||
|
||||
if (volts < minVolts)
|
||||
volts = minVolts
|
||||
else if (volts > maxVolts)
|
||||
volts = maxVolts
|
||||
def pct = batteryMap[volts]
|
||||
if (pct != null) {
|
||||
result.value = pct
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
}
|
||||
else {
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
result.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,20 +318,18 @@ def configure() {
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 300 3600 {01}",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200"
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
@@ -322,12 +337,12 @@ def enrollResponse() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
[
|
||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
@@ -339,19 +354,19 @@ private hex(value) {
|
||||
}
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
int i = 0;
|
||||
int j = array.length - 1;
|
||||
byte tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
return array
|
||||
}
|
||||
int i = 0;
|
||||
int j = array.length - 1;
|
||||
byte tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ metadata {
|
||||
}
|
||||
|
||||
preferences {
|
||||
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
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) {
|
||||
@@ -292,18 +292,16 @@ def configure() {
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 300 3600 {01}",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200"
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
@@ -313,7 +311,7 @@ def enrollResponse() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
[
|
||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
|
||||
@@ -14,27 +14,28 @@
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
capability "Three Axis"
|
||||
metadata {
|
||||
definition (name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
capability "Three Axis"
|
||||
capability "Battery"
|
||||
capability "Configuration"
|
||||
capability "Sensor"
|
||||
capability "Configuration"
|
||||
capability "Sensor"
|
||||
capability "Contact Sensor"
|
||||
capability "Acceleration Sensor"
|
||||
capability "Refresh"
|
||||
capability "Temperature Measurement"
|
||||
|
||||
|
||||
command "enrollResponse"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S"
|
||||
fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S", deviceJoinName: "Multipurpose Sensor"
|
||||
fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500,FC02", outClusters: "0019", manufacturer: "SmartThings", model: "multiv4", deviceJoinName: "Multipurpose Sensor"
|
||||
|
||||
attribute "status", "string"
|
||||
}
|
||||
}
|
||||
|
||||
simulator {
|
||||
simulator {
|
||||
status "open": "zone report :: type: 19 value: 0031"
|
||||
status "closed": "zone report :: type: 19 value: 0030"
|
||||
|
||||
@@ -51,23 +52,23 @@
|
||||
status "x,y,z: 0,1000,0": "x: 0, y: 1000, z: 0"
|
||||
status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000"
|
||||
}
|
||||
preferences {
|
||||
preferences {
|
||||
section {
|
||||
image(name: 'educationalcontent', multiple: true, images: [
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi1.png",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi2.png",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi3.png",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi4.png"
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi1.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi2.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi3.jpg",
|
||||
"http://cdn.device-gse.smartthings.com/Multi/Multi4.jpg"
|
||||
])
|
||||
}
|
||||
section {
|
||||
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
}
|
||||
section {
|
||||
input("garageSensor", "enum", title: "Do you want to use this sensor on a garage door?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false)
|
||||
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?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false)
|
||||
}
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){
|
||||
@@ -99,107 +100,117 @@
|
||||
]
|
||||
)
|
||||
}
|
||||
valueTile("3axis", "device.threeAxis", decoration: "flat", wordWrap: false, width: 2, height: 2) {
|
||||
state("threeAxis", label:'${currentValue}', unit:"", backgroundColor:"#ffffff")
|
||||
}
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
|
||||
main(["status", "acceleration", "temperature"])
|
||||
details(["status", "acceleration", "temperature", "3axis", "battery", "refresh"])
|
||||
details(["status", "acceleration", "temperature", "battery", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
def parse(String 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)
|
||||
}
|
||||
else if (description?.startsWith('zone status')) {
|
||||
map = parseIasMessage(description)
|
||||
}
|
||||
def parse(String description) {
|
||||
Map map = [:]
|
||||
if (description?.startsWith('catchall:')) {
|
||||
map = parseCatchAllMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('temperature: ')) {
|
||||
map = parseCustomMessage(description)
|
||||
}
|
||||
else if (description?.startsWith('zone status')) {
|
||||
map = parseIasMessage(description)
|
||||
}
|
||||
|
||||
def result = map ? createEvent(map) : null
|
||||
def result = map ? createEvent(map) : null
|
||||
|
||||
if (description?.startsWith('enroll request')) {
|
||||
List cmds = enrollResponse()
|
||||
log.debug "enroll response: ${cmds}"
|
||||
result = cmds?.collect { new physicalgraph.device.HubAction(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:
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
break
|
||||
|
||||
case 0xFC02:
|
||||
log.debug 'ACCELERATION'
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
log.debug 'TEMP'
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
return resultMap
|
||||
}
|
||||
|
||||
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
|
||||
if (description?.startsWith('enroll request')) {
|
||||
List cmds = enrollResponse()
|
||||
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 parseReportAttributeMessage(String description) {
|
||||
private Map parseCatchAllMessage(String description) {
|
||||
Map resultMap = [:]
|
||||
def cluster = zigbee.parse(description)
|
||||
log.debug cluster
|
||||
if (shouldProcessMessage(cluster)) {
|
||||
switch(cluster.clusterId) {
|
||||
case 0x0001:
|
||||
resultMap = getBatteryResult(cluster.data.last())
|
||||
break
|
||||
|
||||
case 0xFC02:
|
||||
log.debug 'ACCELERATION'
|
||||
break
|
||||
|
||||
case 0x0402:
|
||||
log.debug 'TEMP'
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
return resultMap
|
||||
}
|
||||
|
||||
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 List parseReportAttributeMessage(String description) {
|
||||
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
|
||||
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 == "0010") {
|
||||
resultMap = getAccelerationResult(descMap.value)
|
||||
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") {
|
||||
resultMap = parseAxis(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) {
|
||||
@@ -217,43 +228,43 @@ private Map parseIasMessage(String description) {
|
||||
|
||||
Map resultMap = [:]
|
||||
switch(msgCode) {
|
||||
case '0x0020': // Closed/No Motion/Dry
|
||||
case '0x0020': // Closed/No Motion/Dry
|
||||
if (garageSensor != "Yes"){
|
||||
resultMap = getContactResult('closed')
|
||||
}
|
||||
break
|
||||
break
|
||||
|
||||
case '0x0021': // Open/Motion/Wet
|
||||
case '0x0021': // Open/Motion/Wet
|
||||
if (garageSensor != "Yes"){
|
||||
resultMap = getContactResult('open')
|
||||
}
|
||||
break
|
||||
break
|
||||
|
||||
case '0x0022': // Tamper Alarm
|
||||
break
|
||||
case '0x0022': // Tamper Alarm
|
||||
break
|
||||
|
||||
case '0x0023': // Battery Alarm
|
||||
break
|
||||
case '0x0023': // Battery Alarm
|
||||
break
|
||||
|
||||
case '0x0024': // Supervision Report
|
||||
case '0x0024': // Supervision Report
|
||||
if (garageSensor != "Yes"){
|
||||
resultMap = getContactResult('closed')
|
||||
}
|
||||
break
|
||||
break
|
||||
|
||||
case '0x0025': // Restore Report
|
||||
case '0x0025': // Restore Report
|
||||
if (garageSensor != "Yes"){
|
||||
resultMap = getContactResult('open')
|
||||
}
|
||||
break
|
||||
break
|
||||
|
||||
case '0x0026': // Trouble/Failure
|
||||
break
|
||||
case '0x0026': // Trouble/Failure
|
||||
break
|
||||
|
||||
case '0x0028': // Test Mode
|
||||
break
|
||||
}
|
||||
return resultMap
|
||||
case '0x0028': // Test Mode
|
||||
break
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
def updated() {
|
||||
@@ -288,147 +299,171 @@ def getTemperature(value) {
|
||||
}
|
||||
}
|
||||
|
||||
private Map getBatteryResult(rawValue) {
|
||||
log.debug "Battery"
|
||||
log.debug rawValue
|
||||
def linkText = getLinkText(device)
|
||||
private Map getBatteryResult(rawValue) {
|
||||
log.debug "Battery rawValue = ${rawValue}"
|
||||
def linkText = getLinkText(device)
|
||||
|
||||
def result = [
|
||||
def result = [
|
||||
name: 'battery',
|
||||
value: '--'
|
||||
]
|
||||
value: '--'
|
||||
]
|
||||
|
||||
def volts = rawValue / 10
|
||||
def descriptionText
|
||||
|
||||
if (rawValue == 255) {}
|
||||
else {
|
||||
|
||||
if (volts > 3.5) {
|
||||
def volts = rawValue / 10
|
||||
|
||||
if (rawValue == 0 || rawValue == 255) {}
|
||||
else {
|
||||
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)
|
||||
result.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.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,
|
||||
22:70, 21:50, 20:50, 19:30, 18:30, 17:15, 16:1, 15:0]
|
||||
def minVolts = 15
|
||||
def maxVolts = 28
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private Map getTemperatureResult(value) {
|
||||
log.debug "Temperature"
|
||||
def linkText = getLinkText(device)
|
||||
if (tempOffset) {
|
||||
def offset = tempOffset as int
|
||||
def v = value as int
|
||||
value = v + offset
|
||||
if (volts < minVolts)
|
||||
volts = minVolts
|
||||
else if (volts > maxVolts)
|
||||
volts = maxVolts
|
||||
def pct = batteryMap[volts]
|
||||
if (pct != null) {
|
||||
result.value = pct
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
}
|
||||
else {
|
||||
def minVolts = 2.1
|
||||
def maxVolts = 3.0
|
||||
def pct = (volts - minVolts) / (maxVolts - minVolts)
|
||||
result.value = Math.min(100, (int) pct * 100)
|
||||
result.descriptionText = "${linkText} battery was ${result.value}%"
|
||||
}
|
||||
}
|
||||
def descriptionText = "${linkText} was ${value}°${temperatureScale}"
|
||||
return [
|
||||
name: 'temperature',
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private Map getTemperatureResult(value) {
|
||||
log.debug "Temperature"
|
||||
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}"
|
||||
return [
|
||||
name: 'temperature',
|
||||
value: value,
|
||||
descriptionText: descriptionText
|
||||
]
|
||||
}
|
||||
|
||||
private Map getContactResult(value) {
|
||||
log.debug "Contact"
|
||||
def linkText = getLinkText(device)
|
||||
def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}"
|
||||
sendEvent(name: 'contact', value: value, descriptionText: descriptionText, displayed:false)
|
||||
sendEvent(name: 'status', value: value, descriptionText: descriptionText)
|
||||
}
|
||||
|
||||
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 isStateChange = isStateChange(device, name, value)
|
||||
[
|
||||
name: name,
|
||||
value: value,
|
||||
descriptionText: descriptionText
|
||||
descriptionText: descriptionText,
|
||||
isStateChange: isStateChange
|
||||
]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Refreshing Values "
|
||||
|
||||
def refreshCmds = []
|
||||
|
||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||
log.debug "Refreshing Values for manufacturer: SmartThings "
|
||||
refreshCmds = refreshCmds + [
|
||||
/* These values of Motion Threshold Multiplier(01) and Motion Threshold (7602)
|
||||
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.
|
||||
*/
|
||||
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global write 0xFC02 0 0x20 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
||||
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global write 0xFC02 2 0x21 {7602}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
||||
]
|
||||
} else {
|
||||
refreshCmds = refreshCmds + [
|
||||
/* sensitivity - default value (8) */
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global write 0xFC02 0 0x20 {02}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
||||
]
|
||||
}
|
||||
|
||||
private Map getContactResult(value) {
|
||||
log.debug "Contact"
|
||||
def linkText = getLinkText(device)
|
||||
def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}"
|
||||
sendEvent(name: 'contact', value: value, descriptionText: descriptionText, displayed:false)
|
||||
sendEvent(name: 'status', value: value, descriptionText: descriptionText)
|
||||
}
|
||||
|
||||
private getAccelerationResult(numValue) {
|
||||
log.debug "Acceleration"
|
||||
def name = "acceleration"
|
||||
def value = numValue.endsWith("1") ? "active" : "inactive"
|
||||
//def linkText = getLinkText(device)
|
||||
def descriptionText = "was $value"
|
||||
def isStateChange = isStateChange(device, name, value)
|
||||
[
|
||||
name: name,
|
||||
value: value,
|
||||
descriptionText: descriptionText,
|
||||
isStateChange: isStateChange
|
||||
]
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Refreshing Values "
|
||||
def refreshCmds = [
|
||||
|
||||
/* sensitivity - default value (8) */
|
||||
|
||||
"zcl mfg-code 0x104E", "delay 200",
|
||||
"zcl global write 0xFC02 0 0x20 {02}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400",
|
||||
|
||||
//Common refresh commands
|
||||
refreshCmds = refreshCmds + [
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200",
|
||||
|
||||
"zcl mfg-code 0x104E", "delay 200",
|
||||
"zcl global read 0xFC02 0x0010",
|
||||
"send 0x${device.deviceNetworkId} 1 1","delay 400",
|
||||
|
||||
"zcl mfg-code 0x104E", "delay 200",
|
||||
"zcl global read 0xFC02 0x0012",
|
||||
"send 0x${device.deviceNetworkId} 1 1","delay 400",
|
||||
|
||||
"zcl mfg-code 0x104E", "delay 200",
|
||||
"zcl global read 0xFC02 0x0013",
|
||||
"send 0x${device.deviceNetworkId} 1 1","delay 400",
|
||||
|
||||
"zcl mfg-code 0x104E", "delay 200",
|
||||
"zcl global read 0xFC02 0x0014",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 400"
|
||||
]
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global read 0xFC02 0x0010",
|
||||
"send 0x${device.deviceNetworkId} 1 1","delay 400"
|
||||
]
|
||||
|
||||
return refreshCmds + enrollResponse()
|
||||
}
|
||||
return refreshCmds + enrollResponse()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
def configure() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting"
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting"
|
||||
|
||||
def configCmds = [
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", "delay 200", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0xFC02 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0010 0x18 300 3600 {01}",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global send-me-a-report 0xFC02 0x0010 0x18 10 3600 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0012 0x29 300 3600 {01}",
|
||||
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global send-me-a-report 0xFC02 0x0012 0x29 1 3600 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0013 0x29 300 3600 {01}",
|
||||
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global send-me-a-report 0xFC02 0x0013 0x29 1 3600 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zcl mfg-code 0x104E",
|
||||
"zcl global send-me-a-report 0xFC02 0x0014 0x29 300 3600 {01}",
|
||||
|
||||
"zcl mfg-code ${manufacturerCode}", "delay 200",
|
||||
"zcl global send-me-a-report 0xFC02 0x0014 0x29 1 3600 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||
|
||||
]
|
||||
|
||||
return configCmds + refresh()
|
||||
|
||||
return configCmds + refresh()
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
@@ -440,43 +475,42 @@ def enrollResponse() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
[
|
||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 200"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
private Map parseAxis(String description) {
|
||||
log.debug "parseAxis"
|
||||
def xyzResults = [x: 0, y: 0, z: 0]
|
||||
def parts = description.split("2900")
|
||||
parts[0] = "12" + parts[0]
|
||||
parts.each { part ->
|
||||
part = part.trim()
|
||||
if (part.startsWith("12")) {
|
||||
def unsignedX = hexToInt(part.split("12")[1].trim())
|
||||
def signedX = unsignedX > 32767 ? unsignedX - 65536 : unsignedX
|
||||
xyzResults.x = signedX
|
||||
log.debug "X Part: ${signedX}"
|
||||
}
|
||||
else if (part.startsWith("13")) {
|
||||
def unsignedY = hexToInt(part.split("13")[1].trim())
|
||||
def signedY = unsignedY > 32767 ? unsignedY - 65536 : unsignedY
|
||||
xyzResults.y = signedY
|
||||
log.debug "Y Part: ${signedY}"
|
||||
}
|
||||
else if (part.startsWith("14")) {
|
||||
def unsignedZ = hexToInt(part.split("14")[1].trim())
|
||||
def signedZ = unsignedZ > 32767 ? unsignedZ - 65536 : unsignedZ
|
||||
xyzResults.z = signedZ
|
||||
log.debug "Z Part: ${signedZ}"
|
||||
if (garageSensor == "Yes")
|
||||
garageEvent(signedZ)
|
||||
}
|
||||
}
|
||||
def hexToSignedInt = { hexVal ->
|
||||
def unsignedVal = hexToInt(hexVal)
|
||||
unsignedVal > 32767 ? unsignedVal - 65536 : unsignedVal
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -520,6 +554,14 @@ private Map getXyzResult(results, description) {
|
||||
]
|
||||
}
|
||||
|
||||
private getManufacturerCode() {
|
||||
if (device.getDataValue("manufacturer") == "SmartThings") {
|
||||
return "0x110A"
|
||||
} else {
|
||||
return "0x104E"
|
||||
}
|
||||
}
|
||||
|
||||
private hexToInt(value) {
|
||||
new BigInteger(value, 16)
|
||||
}
|
||||
@@ -545,4 +587,3 @@ private byte[] reverseArray(byte[] array) {
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ metadata {
|
||||
}
|
||||
|
||||
preferences {
|
||||
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
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) {
|
||||
@@ -72,15 +72,12 @@ metadata {
|
||||
]
|
||||
)
|
||||
}
|
||||
valueTile("3axis", "device.threeAxis", decoration: "flat", wordWrap: false, width: 2, height: 2) {
|
||||
state("threeAxis", label:'${currentValue}', unit:"", backgroundColor:"#ffffff")
|
||||
}
|
||||
valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
|
||||
main(["contact", "acceleration", "temperature"])
|
||||
details(["contact", "acceleration", "temperature", "3axis", "battery"])
|
||||
details(["contact", "acceleration", "temperature", "battery"])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
}
|
||||
|
||||
preferences {
|
||||
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
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) {
|
||||
@@ -297,29 +297,27 @@ def getTemperature(value) {
|
||||
return refreshCmds + enrollResponse()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
def configure() {
|
||||
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC02 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0xFC02 2 0x18 300 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0xFC02 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0xFC02 2 0x18 30 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def enrollResponse() {
|
||||
@@ -327,7 +325,7 @@ def enrollResponse() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
[
|
||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
|
||||
@@ -34,8 +34,8 @@ metadata {
|
||||
}
|
||||
|
||||
preferences {
|
||||
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
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) {
|
||||
@@ -275,22 +275,16 @@ def configure() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
log.debug "Configuring Reporting, IAS CIE, and Bindings."
|
||||
def configCmds = [
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
|
||||
//"raw 0x500 {01 23 00 00 00}", "delay 200",
|
||||
//"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500"
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
@@ -300,7 +294,7 @@ def enrollResponse() {
|
||||
String zigbeeEui = swapEndianHex(device.hub.zigbeeEui)
|
||||
[
|
||||
//Resending the CIE in case the enroll request is sent before CIE is written
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}",
|
||||
"zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
|
||||
//Enroll Response
|
||||
"raw 0x500 {01 23 00 00 00}",
|
||||
|
||||
@@ -33,8 +33,8 @@ metadata {
|
||||
}
|
||||
|
||||
preferences {
|
||||
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
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) {
|
||||
@@ -253,22 +253,19 @@ def configure() {
|
||||
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
def configCmds = [
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
|
||||
"zcl global send-me-a-report 1 0x20 0x20 600 3600 {0100}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1000",
|
||||
|
||||
"zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
"zcl global send-me-a-report 0xFC45 0 0x29 300 3600 {6400}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC45 {${device.zigbeeId}} {}", "delay 1000",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}"
|
||||
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0xFC45 {${device.zigbeeId}} {}", "delay 500",
|
||||
"zcl global send-me-a-report 0xFC45 0 0x29 30 3600 {6400}",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 500"
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
private hex(value) {
|
||||
|
||||
@@ -45,8 +45,8 @@ metadata {
|
||||
}
|
||||
|
||||
preferences {
|
||||
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
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 {
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
|
||||
|
||||
metadata {
|
||||
definition (name: "Sylvania Ultra iQ", namespace:"smartthings", author: "SmartThings") {
|
||||
capability "Switch Level"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
metadata {
|
||||
definition (name: "Color Control Capability", namespace: "capabilities", author: "SmartThings") {
|
||||
definition (name: "Simulated Color Control", namespace: "smartthings/testing", author: "SmartThings") {
|
||||
capability "Color Control"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2014 SmartThings
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
@@ -15,6 +15,7 @@ metadata {
|
||||
// Automatically generated. Make future change here.
|
||||
definition (name: "Simulated Thermostat", namespace: "smartthings/testing", author: "SmartThings") {
|
||||
capability "Thermostat"
|
||||
capability "Relative Humidity Measurement"
|
||||
|
||||
command "tempUp"
|
||||
command "tempDown"
|
||||
@@ -22,11 +23,40 @@ metadata {
|
||||
command "heatDown"
|
||||
command "coolUp"
|
||||
command "coolDown"
|
||||
command "setTemperature", ["number"]
|
||||
command "setTemperature", ["number"]
|
||||
}
|
||||
|
||||
tiles {
|
||||
valueTile("temperature", "device.temperature", width: 1, height: 1) {
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"thermostatMulti", type:"thermostat", width:6, height:4) {
|
||||
tileAttribute("device.temperature", key: "PRIMARY_CONTROL") {
|
||||
attributeState("default", label:'${currentValue}', unit:"dF")
|
||||
}
|
||||
tileAttribute("device.temperature", key: "VALUE_CONTROL") {
|
||||
attributeState("default", action: "setTemperature")
|
||||
}
|
||||
tileAttribute("device.humidity", key: "SECONDARY_CONTROL") {
|
||||
attributeState("default", label:'${currentValue}%', unit:"%")
|
||||
}
|
||||
tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") {
|
||||
attributeState("idle", backgroundColor:"#44b621")
|
||||
attributeState("heating", backgroundColor:"#ffa81e")
|
||||
attributeState("cooling", backgroundColor:"#269bd2")
|
||||
}
|
||||
tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") {
|
||||
attributeState("off", label:'${name}')
|
||||
attributeState("heat", label:'${name}')
|
||||
attributeState("cool", label:'${name}')
|
||||
attributeState("auto", label:'${name}')
|
||||
}
|
||||
tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") {
|
||||
attributeState("default", label:'${currentValue}', unit:"dF")
|
||||
}
|
||||
tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") {
|
||||
attributeState("default", label:'${currentValue}', unit:"dF")
|
||||
}
|
||||
}
|
||||
|
||||
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||
state("temperature", label:'${currentValue}', unit:"dF",
|
||||
backgroundColors:[
|
||||
[value: 31, color: "#153591"],
|
||||
@@ -39,51 +69,51 @@ metadata {
|
||||
]
|
||||
)
|
||||
}
|
||||
standardTile("tempDown", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
||||
standardTile("tempDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'down', action:"tempDown"
|
||||
}
|
||||
standardTile("tempUp", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
||||
standardTile("tempUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'up', action:"tempUp"
|
||||
}
|
||||
|
||||
valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||
valueTile("heatingSetpoint", "device.heatingSetpoint", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "heat", label:'${currentValue} heat', unit: "F", backgroundColor:"#ffffff"
|
||||
}
|
||||
standardTile("heatDown", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
||||
standardTile("heatDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'down', action:"heatDown"
|
||||
}
|
||||
standardTile("heatUp", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
||||
standardTile("heatUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'up', action:"heatUp"
|
||||
}
|
||||
|
||||
valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") {
|
||||
valueTile("coolingSetpoint", "device.coolingSetpoint", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "cool", label:'${currentValue} cool', unit:"F", backgroundColor:"#ffffff"
|
||||
}
|
||||
standardTile("coolDown", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
||||
standardTile("coolDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'down', action:"coolDown"
|
||||
}
|
||||
standardTile("coolUp", "device.temperature", inactiveLabel: false, decoration: "flat") {
|
||||
standardTile("coolUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'up', action:"coolUp"
|
||||
}
|
||||
|
||||
standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") {
|
||||
standardTile("mode", "device.thermostatMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "off", label:'${name}', action:"thermostat.heat", backgroundColor:"#ffffff"
|
||||
state "heat", label:'${name}', action:"thermostat.cool", backgroundColor:"#ffa81e"
|
||||
state "cool", label:'${name}', action:"thermostat.auto", backgroundColor:"#269bd2"
|
||||
state "auto", label:'${name}', action:"thermostat.off", backgroundColor:"#79b821"
|
||||
}
|
||||
standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") {
|
||||
standardTile("fanMode", "device.thermostatFanMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "fanAuto", label:'${name}', action:"thermostat.fanOn", backgroundColor:"#ffffff"
|
||||
state "fanOn", label:'${name}', action:"thermostat.fanCirculate", backgroundColor:"#ffffff"
|
||||
state "fanCirculate", label:'${name}', action:"thermostat.fanAuto", backgroundColor:"#ffffff"
|
||||
}
|
||||
standardTile("operatingState", "device.thermostatOperatingState") {
|
||||
standardTile("operatingState", "device.thermostatOperatingState", width: 2, height: 2) {
|
||||
state "idle", label:'${name}', backgroundColor:"#ffffff"
|
||||
state "heating", label:'${name}', backgroundColor:"#ffa81e"
|
||||
state "cooling", label:'${name}', backgroundColor:"#269bd2"
|
||||
}
|
||||
|
||||
main("temperature","operatingState")
|
||||
main("thermostatMulti")
|
||||
details([
|
||||
"temperature","tempDown","tempUp",
|
||||
"mode", "fanMode", "operatingState",
|
||||
@@ -101,6 +131,7 @@ def installed() {
|
||||
sendEvent(name: "thermostatMode", value: "off")
|
||||
sendEvent(name: "thermostatFanMode", value: "fanAuto")
|
||||
sendEvent(name: "thermostatOperatingState", value: "idle")
|
||||
sendEvent(name: "humidity", value: 53, unit: "%")
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
|
||||
@@ -33,8 +33,8 @@ metadata {
|
||||
}
|
||||
|
||||
preferences {
|
||||
input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph"
|
||||
input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false
|
||||
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 {
|
||||
|
||||
@@ -29,18 +29,18 @@ metadata {
|
||||
|
||||
tiles {
|
||||
|
||||
valueTile("power", "device.power") {
|
||||
valueTile("power", "device.power", canChangeIcon: true) {
|
||||
state "power", label: '${currentValue} W'
|
||||
}
|
||||
|
||||
tile(name: "powerChart", attribute: "powerContent", type: "HTML", url: '${currentValue}', width: 3, height: 2) { }
|
||||
htmlTile(name: "powerContent", attribute: "powerContent", type: "HTML", whitelist: "www.wattvision.com" , url: '${currentValue}', width: 3, height: 2)
|
||||
|
||||
standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh"
|
||||
}
|
||||
|
||||
main "power"
|
||||
details(["powerChart", "power", "refresh"])
|
||||
details(["powerContent", "power", "refresh"])
|
||||
|
||||
}
|
||||
}
|
||||
@@ -74,10 +74,10 @@ public addWattvisionData(json) {
|
||||
|
||||
log.trace "Adding data from Wattvision"
|
||||
|
||||
def data = json.data
|
||||
def data = parseJson(json.data.toString())
|
||||
def units = json.units ?: "watts"
|
||||
|
||||
if (data) {
|
||||
if (data.size() > 0) {
|
||||
def latestData = data[-1]
|
||||
data.each {
|
||||
sendPowerEvent(it.t, it.v, units, (latestData == it))
|
||||
@@ -103,3 +103,7 @@ private sendPowerEvent(time, value, units, isLatest = false) {
|
||||
sendEvent(eventData)
|
||||
|
||||
}
|
||||
|
||||
def parseJson(String s) {
|
||||
new groovy.json.JsonSlurper().parseText(s)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
* Thanks to Chad Monroe @cmonroe and Patrick Stuart @pstuart
|
||||
*
|
||||
*/
|
||||
//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH
|
||||
|
||||
metadata {
|
||||
definition (name: "WeMo Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
@@ -25,7 +27,6 @@ metadata {
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,FF00", outClusters: "0019"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
|
||||
@@ -25,6 +25,8 @@ metadata {
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
|
||||
attribute "currentIP", "string"
|
||||
|
||||
command "subscribe"
|
||||
command "resubscribe"
|
||||
command "unsubscribe"
|
||||
@@ -34,21 +36,36 @@ metadata {
|
||||
// simulator metadata
|
||||
simulator {}
|
||||
|
||||
// UI tile definitions
|
||||
tiles {
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
state "turningOn", label:'${name}', icon:"st.switches.switch.on", backgroundColor:"#79b821"
|
||||
state "turningOff", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ffffff"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
// UI tile definitions
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"rich-control", type: "switch", canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "offline", label:'${name}', icon:"st.Home.home30", backgroundColor:"#ff0000"
|
||||
}
|
||||
tileAttribute ("currentIP", key: "SECONDARY_CONTROL") {
|
||||
attributeState "currentIP", label: ''
|
||||
}
|
||||
}
|
||||
|
||||
main "switch"
|
||||
details (["switch", "refresh"])
|
||||
}
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "off", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
state "turningOn", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "turningOff", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
state "offline", label:'${name}', icon:"st.Home.home30", backgroundColor:"#ff0000"
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["rich-control", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
@@ -68,6 +85,7 @@ def parse(String description) {
|
||||
def result = []
|
||||
def bodyString = msg.body
|
||||
if (bodyString) {
|
||||
unschedule("setOffline")
|
||||
def body = new XmlSlurper().parseText(bodyString)
|
||||
|
||||
if (body?.property?.TimeSyncRequest?.text()) {
|
||||
@@ -78,13 +96,14 @@ def parse(String description) {
|
||||
} else if (body?.property?.BinaryState?.text()) {
|
||||
def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
|
||||
log.trace "Notify: BinaryState = ${value}"
|
||||
result << createEvent(name: "switch", value: value)
|
||||
result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}")
|
||||
} else if (body?.property?.TimeZoneNotification?.text()) {
|
||||
log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}"
|
||||
} else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) {
|
||||
def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
|
||||
log.trace "GetBinaryResponse: BinaryState = ${value}"
|
||||
result << createEvent(name: "switch", value: value)
|
||||
def dispaux = device.currentValue("switch") != value
|
||||
result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,14 +120,6 @@ private getCallBackAddress() {
|
||||
device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP")
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
private String convertHexToIP(hex) {
|
||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||
}
|
||||
|
||||
private getHostAddress() {
|
||||
def ip = getDataValue("ip")
|
||||
def port = getDataValue("port")
|
||||
@@ -195,6 +206,8 @@ def subscribe(ip, port) {
|
||||
if (ip && ip != existingIp) {
|
||||
log.debug "Updating ip from $existingIp to $ip"
|
||||
updateDataValue("ip", ip)
|
||||
def ipvalue = convertHexToIP(getDataValue("ip"))
|
||||
sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}")
|
||||
}
|
||||
if (port && port != existingPort) {
|
||||
log.debug "Updating port from $existingPort to $port"
|
||||
@@ -259,6 +272,8 @@ User-Agent: CyberGarage-HTTP/1.0
|
||||
|
||||
def poll() {
|
||||
log.debug "Executing 'poll'"
|
||||
if (device.currentValue("currentIP") != "Offline")
|
||||
runIn(30, setOffline)
|
||||
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
||||
Content-Length: 277
|
||||
@@ -274,3 +289,15 @@ User-Agent: CyberGarage-HTTP/1.0
|
||||
</s:Body>
|
||||
</s:Envelope>""", physicalgraph.device.Protocol.LAN)
|
||||
}
|
||||
|
||||
def setOffline() {
|
||||
sendEvent(name: "switch", value: "offline", descriptionText: "The device is offline")
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
private String convertHexToIP(hex) {
|
||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
|
||||
attribute "currentIP", "string"
|
||||
|
||||
command "subscribe"
|
||||
command "resubscribe"
|
||||
command "unsubscribe"
|
||||
@@ -31,17 +33,30 @@
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles {
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"rich-control", type: "motion", canChangeIcon: true){
|
||||
tileAttribute ("device.motion", key: "PRIMARY_CONTROL") {
|
||||
attributeState "active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0"
|
||||
attributeState "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff"
|
||||
attributeState "offline", label:'${name}', icon:"st.motion.motion.active", backgroundColor:"#ff0000"
|
||||
}
|
||||
tileAttribute ("currentIP", key: "SECONDARY_CONTROL") {
|
||||
attributeState "currentIP", label: ''
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("motion", "device.motion", width: 2, height: 2) {
|
||||
state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0")
|
||||
state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff")
|
||||
}
|
||||
standardTile("refresh", "device.motion", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
state("offline", label:'${name}', icon:"st.motion.motion.inactive", backgroundColor:"#ff0000")
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main "motion"
|
||||
details (["motion", "refresh"])
|
||||
details (["rich-control", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,8 +77,8 @@ def parse(String description) {
|
||||
def result = []
|
||||
def bodyString = msg.body
|
||||
if (bodyString) {
|
||||
unschedule("setOffline")
|
||||
def body = new XmlSlurper().parseText(bodyString)
|
||||
|
||||
if (body?.property?.TimeSyncRequest?.text()) {
|
||||
log.trace "Got TimeSyncRequest"
|
||||
result << timeSyncResponse()
|
||||
@@ -72,7 +87,7 @@ def parse(String description) {
|
||||
} else if (body?.property?.BinaryState?.text()) {
|
||||
def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "active" : "inactive"
|
||||
log.debug "Notify - BinaryState = ${value}"
|
||||
result << createEvent(name: "motion", value: value)
|
||||
result << createEvent(name: "motion", value: value, descriptionText: "Motion is ${value}")
|
||||
} else if (body?.property?.TimeZoneNotification?.text()) {
|
||||
log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}"
|
||||
}
|
||||
@@ -91,14 +106,6 @@ private getCallBackAddress() {
|
||||
device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP")
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
private String convertHexToIP(hex) {
|
||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||
}
|
||||
|
||||
private getHostAddress() {
|
||||
def ip = getDataValue("ip")
|
||||
def port = getDataValue("port")
|
||||
@@ -125,6 +132,8 @@ def refresh() {
|
||||
////////////////////////////
|
||||
def getStatus() {
|
||||
log.debug "Executing WeMo Motion 'getStatus'"
|
||||
if (device.currentValue("currentIP") != "Offline")
|
||||
runIn(30, setOffline)
|
||||
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
||||
Content-Length: 277
|
||||
@@ -165,7 +174,9 @@ def subscribe(ip, port) {
|
||||
def existingPort = getDataValue("port")
|
||||
if (ip && ip != existingIp) {
|
||||
log.debug "Updating ip from $existingIp to $ip"
|
||||
updateDataValue("ip", ip)
|
||||
updateDataValue("ip", ip)
|
||||
def ipvalue = convertHexToIP(getDataValue("ip"))
|
||||
sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}")
|
||||
}
|
||||
if (port && port != existingPort) {
|
||||
log.debug "Updating port from $existingPort to $port"
|
||||
@@ -226,3 +237,15 @@ User-Agent: CyberGarage-HTTP/1.0
|
||||
</s:Envelope>
|
||||
""", physicalgraph.device.Protocol.LAN)
|
||||
}
|
||||
|
||||
def setOffline() {
|
||||
sendEvent(name: "motion", value: "offline", descriptionText: "The device is offline")
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
private String convertHexToIP(hex) {
|
||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||
}
|
||||
|
||||
@@ -10,120 +10,143 @@
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* Wemo Switch
|
||||
* Wemo Switch
|
||||
*
|
||||
* Author: superuser
|
||||
* Date: 2013-10-11
|
||||
* Author: Juan Risso (SmartThings)
|
||||
* Date: 2015-10-11
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Wemo Switch", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Switch"
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
definition (name: "Wemo Switch", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Switch"
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
|
||||
command "subscribe"
|
||||
command "resubscribe"
|
||||
command "unsubscribe"
|
||||
}
|
||||
attribute "currentIP", "string"
|
||||
|
||||
// simulator metadata
|
||||
simulator {}
|
||||
command "subscribe"
|
||||
command "resubscribe"
|
||||
command "unsubscribe"
|
||||
command "setOffline"
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles {
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821"
|
||||
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
// simulator metadata
|
||||
simulator {}
|
||||
|
||||
main "switch"
|
||||
details (["switch", "refresh"])
|
||||
}
|
||||
// UI tile definitions
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"rich-control", type: "switch", canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "offline", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ff0000"
|
||||
}
|
||||
tileAttribute ("currentIP", key: "SECONDARY_CONTROL") {
|
||||
attributeState "currentIP", label: ''
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
state "offline", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ff0000"
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["rich-control", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
log.debug "Parsing '${description}'"
|
||||
|
||||
def msg = parseLanMessage(description)
|
||||
def headerString = msg.header
|
||||
def msg = parseLanMessage(description)
|
||||
def headerString = msg.header
|
||||
|
||||
if (headerString?.contains("SID: uuid:")) {
|
||||
def sid = (headerString =~ /SID: uuid:.*/) ? ( headerString =~ /SID: uuid:.*/)[0] : "0"
|
||||
sid -= "SID: uuid:".trim()
|
||||
if (headerString?.contains("SID: uuid:")) {
|
||||
def sid = (headerString =~ /SID: uuid:.*/) ? ( headerString =~ /SID: uuid:.*/)[0] : "0"
|
||||
sid -= "SID: uuid:".trim()
|
||||
|
||||
updateDataValue("subscriptionId", sid)
|
||||
}
|
||||
updateDataValue("subscriptionId", sid)
|
||||
}
|
||||
|
||||
def result = []
|
||||
def bodyString = msg.body
|
||||
if (bodyString) {
|
||||
def body = new XmlSlurper().parseText(bodyString)
|
||||
|
||||
if (body?.property?.TimeSyncRequest?.text()) {
|
||||
log.trace "Got TimeSyncRequest"
|
||||
result << timeSyncResponse()
|
||||
} else if (body?.Body?.SetBinaryStateResponse?.BinaryState?.text()) {
|
||||
log.trace "Got SetBinaryStateResponse = ${body?.Body?.SetBinaryStateResponse?.BinaryState?.text()}"
|
||||
} else if (body?.property?.BinaryState?.text()) {
|
||||
def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
|
||||
log.trace "Notify: BinaryState = ${value}"
|
||||
result << createEvent(name: "switch", value: value)
|
||||
} else if (body?.property?.TimeZoneNotification?.text()) {
|
||||
log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}"
|
||||
} else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) {
|
||||
def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().toInteger() == 1 ? "on" : "off"
|
||||
log.trace "GetBinaryResponse: BinaryState = ${value}"
|
||||
result << createEvent(name: "switch", value: value)
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
def result = []
|
||||
def bodyString = msg.body
|
||||
if (bodyString) {
|
||||
unschedule("setOffline")
|
||||
def body = new XmlSlurper().parseText(bodyString)
|
||||
if (body?.property?.TimeSyncRequest?.text()) {
|
||||
log.trace "Got TimeSyncRequest"
|
||||
result << timeSyncResponse()
|
||||
} else if (body?.Body?.SetBinaryStateResponse?.BinaryState?.text()) {
|
||||
log.trace "Got SetBinaryStateResponse = ${body?.Body?.SetBinaryStateResponse?.BinaryState?.text()}"
|
||||
} else if (body?.property?.BinaryState?.text()) {
|
||||
def value = body?.property?.BinaryState?.text().substring(0, 1).toInteger() == 0 ? "off" : "on"
|
||||
log.trace "Notify: BinaryState = ${value}, ${body.property.BinaryState}"
|
||||
def dispaux = device.currentValue("switch") != value
|
||||
result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux)
|
||||
} else if (body?.property?.TimeZoneNotification?.text()) {
|
||||
log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}"
|
||||
} else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) {
|
||||
def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().substring(0, 1).toInteger() == 0 ? "off" : "on"
|
||||
log.trace "GetBinaryResponse: BinaryState = ${value}, ${body.property.BinaryState}"
|
||||
log.info "Connection: ${device.currentValue("connection")}"
|
||||
if (device.currentValue("currentIP") == "Offline") {
|
||||
def ipvalue = convertHexToIP(getDataValue("ip"))
|
||||
sendEvent(name: "IP", value: ipvalue, descriptionText: "IP is ${ipvalue}")
|
||||
}
|
||||
def dispaux2 = device.currentValue("switch") != value
|
||||
result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux2)
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
private getTime() {
|
||||
// This is essentially System.currentTimeMillis()/1000, but System is disallowed by the sandbox.
|
||||
((new GregorianCalendar().time.time / 1000l).toInteger()).toString()
|
||||
// This is essentially System.currentTimeMillis()/1000, but System is disallowed by the sandbox.
|
||||
((new GregorianCalendar().time.time / 1000l).toInteger()).toString()
|
||||
}
|
||||
|
||||
private getCallBackAddress() {
|
||||
device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP")
|
||||
device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP")
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
private String convertHexToIP(hex) {
|
||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||
}
|
||||
|
||||
private getHostAddress() {
|
||||
def ip = getDataValue("ip")
|
||||
def port = getDataValue("port")
|
||||
|
||||
if (!ip || !port) {
|
||||
def parts = device.deviceNetworkId.split(":")
|
||||
if (parts.length == 2) {
|
||||
ip = parts[0]
|
||||
port = parts[1]
|
||||
} else {
|
||||
log.warn "Can't figure out ip and port for device: ${device.id}"
|
||||
}
|
||||
}
|
||||
log.debug "Using ip: ${ip} and port: ${port} for device: ${device.id}"
|
||||
return convertHexToIP(ip) + ":" + convertHexToInt(port)
|
||||
def ip = getDataValue("ip")
|
||||
def port = getDataValue("port")
|
||||
if (!ip || !port) {
|
||||
def parts = device.deviceNetworkId.split(":")
|
||||
if (parts.length == 2) {
|
||||
ip = parts[0]
|
||||
port = parts[1]
|
||||
} else {
|
||||
log.warn "Can't figure out ip and port for device: ${device.id}"
|
||||
}
|
||||
}
|
||||
log.debug "Using ip: ${ip} and port: ${port} for device: ${device.id}"
|
||||
return convertHexToIP(ip) + ":" + convertHexToInt(port)
|
||||
}
|
||||
|
||||
|
||||
def on() {
|
||||
log.debug "Executing 'on'"
|
||||
sendEvent(name: "switch", value: "on")
|
||||
log.debug "Executing 'on'"
|
||||
def turnOn = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||
SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState"
|
||||
Host: ${getHostAddress()}
|
||||
@@ -133,17 +156,16 @@ Content-Length: 333
|
||||
<?xml version="1.0"?>
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
||||
<SOAP-ENV:Body>
|
||||
<m:SetBinaryState xmlns:m="urn:Belkin:service:basicevent:1">
|
||||
<m:SetBinaryState xmlns:m="urn:Belkin:service:basicevent:1">
|
||||
<BinaryState>1</BinaryState>
|
||||
</m:SetBinaryState>
|
||||
</m:SetBinaryState>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>""", physicalgraph.device.Protocol.LAN)
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "Executing 'off'"
|
||||
sendEvent(name: "switch", value: "off")
|
||||
def turnOff = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||
log.debug "Executing 'off'"
|
||||
def turnOff = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||
SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState"
|
||||
Host: ${getHostAddress()}
|
||||
Content-Type: text/xml
|
||||
@@ -152,36 +174,13 @@ Content-Length: 333
|
||||
<?xml version="1.0"?>
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
||||
<SOAP-ENV:Body>
|
||||
<m:SetBinaryState xmlns:m="urn:Belkin:service:basicevent:1">
|
||||
<m:SetBinaryState xmlns:m="urn:Belkin:service:basicevent:1">
|
||||
<BinaryState>0</BinaryState>
|
||||
</m:SetBinaryState>
|
||||
</m:SetBinaryState>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>""", physicalgraph.device.Protocol.LAN)
|
||||
}
|
||||
|
||||
/*def refresh() {
|
||||
log.debug "Executing 'refresh'"
|
||||
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
||||
Content-Length: 277
|
||||
Content-Type: text/xml; charset="utf-8"
|
||||
HOST: ${getHostAddress()}
|
||||
User-Agent: CyberGarage-HTTP/1.0
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
||||
<s:Body>
|
||||
<u:GetBinaryState xmlns:u="urn:Belkin:service:basicevent:1">
|
||||
</u:GetBinaryState>
|
||||
</s:Body>
|
||||
</s:Envelope>""", physicalgraph.device.Protocol.LAN)
|
||||
}*/
|
||||
|
||||
def refresh() {
|
||||
log.debug "Executing WeMo Switch 'subscribe', then 'timeSyncResponse', then 'poll'"
|
||||
[subscribe(), timeSyncResponse(), poll()]
|
||||
}
|
||||
|
||||
def subscribe(hostAddress) {
|
||||
log.debug "Executing 'subscribe()'"
|
||||
def address = getCallBackAddress()
|
||||
@@ -200,27 +199,30 @@ def subscribe() {
|
||||
subscribe(getHostAddress())
|
||||
}
|
||||
|
||||
def subscribe(ip, port) {
|
||||
def existingIp = getDataValue("ip")
|
||||
def existingPort = getDataValue("port")
|
||||
if (ip && ip != existingIp) {
|
||||
log.debug "Updating ip from $existingIp to $ip"
|
||||
updateDataValue("ip", ip)
|
||||
}
|
||||
if (port && port != existingPort) {
|
||||
log.debug "Updating port from $existingPort to $port"
|
||||
updateDataValue("port", port)
|
||||
}
|
||||
def refresh() {
|
||||
log.debug "Executing WeMo Switch 'subscribe', then 'timeSyncResponse', then 'poll'"
|
||||
[subscribe(), timeSyncResponse(), poll()]
|
||||
}
|
||||
|
||||
def subscribe(ip, port) {
|
||||
def existingIp = getDataValue("ip")
|
||||
def existingPort = getDataValue("port")
|
||||
if (ip && ip != existingIp) {
|
||||
log.debug "Updating ip from $existingIp to $ip"
|
||||
updateDataValue("ip", ip)
|
||||
def ipvalue = convertHexToIP(getDataValue("ip"))
|
||||
sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}")
|
||||
}
|
||||
if (port && port != existingPort) {
|
||||
log.debug "Updating port from $existingPort to $port"
|
||||
updateDataValue("port", port)
|
||||
}
|
||||
subscribe("${ip}:${port}")
|
||||
}
|
||||
|
||||
////////////////////////////
|
||||
def resubscribe() {
|
||||
log.debug "Executing 'resubscribe()'"
|
||||
|
||||
def sid = getDeviceDataByName("subscriptionId")
|
||||
|
||||
log.debug "Executing 'resubscribe()'"
|
||||
def sid = getDeviceDataByName("subscriptionId")
|
||||
new physicalgraph.device.HubAction("""SUBSCRIBE /upnp/event/basicevent1 HTTP/1.1
|
||||
HOST: ${getHostAddress()}
|
||||
SID: uuid:${sid}
|
||||
@@ -228,12 +230,11 @@ TIMEOUT: Second-5400
|
||||
|
||||
|
||||
""", physicalgraph.device.Protocol.LAN)
|
||||
|
||||
}
|
||||
|
||||
////////////////////////////
|
||||
|
||||
def unsubscribe() {
|
||||
def sid = getDeviceDataByName("subscriptionId")
|
||||
def sid = getDeviceDataByName("subscriptionId")
|
||||
new physicalgraph.device.HubAction("""UNSUBSCRIBE publisher path HTTP/1.1
|
||||
HOST: ${getHostAddress()}
|
||||
SID: uuid:${sid}
|
||||
@@ -242,7 +243,7 @@ SID: uuid:${sid}
|
||||
""", physicalgraph.device.Protocol.LAN)
|
||||
}
|
||||
|
||||
////////////////////////////
|
||||
|
||||
//TODO: Use UTC Timezone
|
||||
def timeSyncResponse() {
|
||||
log.debug "Executing 'timeSyncResponse()'"
|
||||
@@ -267,9 +268,15 @@ User-Agent: CyberGarage-HTTP/1.0
|
||||
""", physicalgraph.device.Protocol.LAN)
|
||||
}
|
||||
|
||||
def setOffline() {
|
||||
//sendEvent(name: "currentIP", value: "Offline", displayed: false)
|
||||
sendEvent(name: "switch", value: "offline", descriptionText: "The device is offline")
|
||||
}
|
||||
|
||||
def poll() {
|
||||
log.debug "Executing 'poll'"
|
||||
if (device.currentValue("currentIP") != "Offline")
|
||||
runIn(30, setOffline)
|
||||
new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1
|
||||
SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState"
|
||||
Content-Length: 277
|
||||
@@ -284,4 +291,4 @@ User-Agent: CyberGarage-HTTP/1.0
|
||||
</u:GetBinaryState>
|
||||
</s:Body>
|
||||
</s:Envelope>""", physicalgraph.device.Protocol.LAN)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "ZigBee Dimmer Power", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Power Meter"
|
||||
capability "Sensor"
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "Z01-CIA19NAE26", deviceJoinName: "Sengled Element touch"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "000A, 0019", manufacturer: "Jasco Products", model: "45852", deviceJoinName: "GE Zigbee Plug-In Dimmer"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "000A, 0019", manufacturer: "Jasco Products", model: "45857", deviceJoinName: "GE Zigbee In-Wall Dimmer"
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel"
|
||||
}
|
||||
tileAttribute ("power", key: "SECONDARY_CONTROL") {
|
||||
attributeState "power", label:'${currentValue} W'
|
||||
}
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
main "switch"
|
||||
details(["switch", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def resultMap = zigbee.getKnownDescription(description)
|
||||
if (resultMap) {
|
||||
log.info resultMap
|
||||
if (resultMap.type == "update") {
|
||||
log.info "$device updates: ${resultMap.value}"
|
||||
}
|
||||
else if (resultMap.type == "power") {
|
||||
def powerValue
|
||||
if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power
|
||||
powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration
|
||||
sendEvent(name: "power", value: powerValue)
|
||||
}
|
||||
}
|
||||
else {
|
||||
sendEvent(name: resultMap.type, value: resultMap.value)
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug zigbee.parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
zigbee.off()
|
||||
}
|
||||
|
||||
def on() {
|
||||
zigbee.on()
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
zigbee.setLevel(value)
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh()
|
||||
}
|
||||
@@ -11,133 +11,79 @@
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "ZigBee Dimmer", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Switch Level"
|
||||
capability "Actuator"
|
||||
capability "Switch"
|
||||
capability "Configuration"
|
||||
capability "Sensor"
|
||||
capability "Refresh"
|
||||
definition (name: "ZigBee Dimmer", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05", outClusters: "0019"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
// status messages
|
||||
status "on": "on/off: 1"
|
||||
status "off": "on/off: 0"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 ON/OFF/DIM", deviceJoinName: "OSRAM LIGHTIFY LED Smart Connected Light"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, FF00", outClusters: "0019", manufacturer: "MRVL", model: "MZ100", deviceJoinName: "Wemo Bulb"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05", outClusters: "0019", manufacturer: "OSRAM SYLVANIA", model: "iQBR30", deviceJoinName: "Sylvania Ultra iQ"
|
||||
}
|
||||
|
||||
// reply messages
|
||||
reply "zcl on-off on": "on/off: 1"
|
||||
reply "zcl on-off off": "on/off: 0"
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel"
|
||||
}
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
valueTile("level", "device.level", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "level", label:'${currentValue} %', unit:"%", backgroundColor:"#ffffff"
|
||||
}
|
||||
main "switch"
|
||||
details(["switch", "refresh", "level", "levelSliderControl"])
|
||||
}
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel"
|
||||
}
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
main "switch"
|
||||
details(["switch", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.info description
|
||||
if (description?.startsWith("catchall:")) {
|
||||
def msg = zigbee.parse(description)
|
||||
log.trace msg
|
||||
log.trace "data: $msg.data"
|
||||
}
|
||||
else {
|
||||
def name = description?.startsWith("on/off: ") ? "switch" : null
|
||||
def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null
|
||||
def result = createEvent(name: name, value: value)
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
}
|
||||
}
|
||||
log.debug "description is $description"
|
||||
|
||||
// Commands to device
|
||||
def on() {
|
||||
log.debug "on()"
|
||||
sendEvent(name: "switch", value: "on")
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}"
|
||||
def resultMap = zigbee.getKnownDescription(description)
|
||||
if (resultMap) {
|
||||
log.info resultMap
|
||||
if (resultMap.type == "update") {
|
||||
log.info "$device updates: ${resultMap.value}"
|
||||
}
|
||||
else {
|
||||
sendEvent(name: resultMap.type, value: resultMap.value)
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug zigbee.parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "off()"
|
||||
sendEvent(name: "switch", value: "off")
|
||||
"st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
|
||||
zigbee.off()
|
||||
}
|
||||
|
||||
def on() {
|
||||
zigbee.on()
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
log.trace "setLevel($value)"
|
||||
def cmds = []
|
||||
|
||||
if (value == 0) {
|
||||
sendEvent(name: "switch", value: "off")
|
||||
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}"
|
||||
}
|
||||
else if (device.latestValue("switch") == "off") {
|
||||
sendEvent(name: "switch", value: "on")
|
||||
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}"
|
||||
|
||||
}
|
||||
|
||||
sendEvent(name: "level", value: value)
|
||||
def level = hexString(Math.round(value * 255/100))
|
||||
cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}"
|
||||
|
||||
//log.debug cmds
|
||||
cmds
|
||||
zigbee.setLevel(value)
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
[
|
||||
"st wattr 0x${device.deviceNetworkId} 1 6 0", "delay 200",
|
||||
"st wattr 0x${device.deviceNetworkId} 1 8 0"
|
||||
]
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() + zigbee.levelConfig()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
|
||||
/*log.debug "binding to switch and level control cluster"
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 200",
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}"
|
||||
]
|
||||
*/
|
||||
|
||||
//set transition time to 2 seconds. Not currently working.
|
||||
"st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {1400}"
|
||||
}
|
||||
|
||||
|
||||
|
||||
private hex(value, width=2) {
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() < width) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
private getEndpointId() {
|
||||
new BigInteger(device.endpointId, 16).toString()
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh()
|
||||
}
|
||||
|
||||
184
devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy
Normal file
184
devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* ZigBee Lock
|
||||
*
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "ZigBee Lock", namespace: "smartthings", author: "SmartThings")
|
||||
{
|
||||
capability "Actuator"
|
||||
capability "Lock"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Battery"
|
||||
capability "Configuration"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019",
|
||||
manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_5", deviceJoinName: "Kwikset 5-Button Deadbolt"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019",
|
||||
manufacturer: "Kwikset", model: "SMARTCODE_LEVER_5", deviceJoinName: "Kwikset 5-Button Lever"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019",
|
||||
manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_10", deviceJoinName: "Kwikset 10-Button Deadbolt"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019",
|
||||
manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_10T", deviceJoinName: "Kwikset 10-Button Touch Deadbolt"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019",
|
||||
manufacturer: "Yale", model: "YRL220 TS LL", deviceJoinName: "Yale Touch Screen Lever Lock"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019",
|
||||
manufacturer: "Yale", model: "YRD210 PB DB", deviceJoinName: "Yale Push Button Deadbolt Lock"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019",
|
||||
manufacturer: "Yale", model: "YRD220/240 TSDB", deviceJoinName: "Yale Touch Screen Deadbolt Lock"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019",
|
||||
manufacturer: "Yale", model: "YRL210 PB LL", deviceJoinName: "Yale Push Button Lever Lock"
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"toggle", type:"generic", width:6, height:4){
|
||||
tileAttribute ("device.lock", key:"PRIMARY_CONTROL") {
|
||||
attributeState "locked", label:'locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#79b821", nextState:"unlocking"
|
||||
attributeState "unlocked", label:'unlocked', action:"lock.lock", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff", nextState:"locking"
|
||||
attributeState "unknown", label:"unknown", action:"lock.lock", icon:"st.locks.lock.unknown", backgroundColor:"#ffffff", nextState:"locking"
|
||||
attributeState "locking", label:'locking', icon:"st.locks.lock.locked", backgroundColor:"#79b821"
|
||||
attributeState "unlocking", label:'unlocking', icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff"
|
||||
}
|
||||
}
|
||||
standardTile("lock", "device.lock", inactiveLabel:false, decoration:"flat", width:2, height:2) {
|
||||
state "default", label:'lock', action:"lock.lock", icon:"st.locks.lock.locked", nextState:"locking"
|
||||
}
|
||||
standardTile("unlock", "device.lock", inactiveLabel:false, decoration:"flat", width:2, height:2) {
|
||||
state "default", label:'unlock', action:"lock.unlock", icon:"st.locks.lock.unlocked", nextState:"unlocking"
|
||||
}
|
||||
valueTile("battery", "device.battery", inactiveLabel:false, decoration:"flat", width:2, height:2) {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
standardTile("refresh", "device.lock", inactiveLabel:false, decoration:"flat", width:2, height:2) {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main "toggle"
|
||||
details(["toggle", "lock", "unlock", "battery", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Globals
|
||||
private getCLUSTER_POWER() { 0x0001 }
|
||||
private getCLUSTER_DOORLOCK() { 0x0101 }
|
||||
|
||||
private getDOORLOCK_CMD_LOCK_DOOR() { 0x00 }
|
||||
private getDOORLOCK_CMD_UNLOCK_DOOR() { 0x01 }
|
||||
private getDOORLOCK_ATTR_LOCKSTATE() { 0x0000 }
|
||||
private getPOWER_ATTR_BATTERY_PERCENTAGE_REMAINING() { 0x0021 }
|
||||
|
||||
private getTYPE_U8() { 0x20 }
|
||||
private getTYPE_ENUM8() { 0x30 }
|
||||
|
||||
// Public methods
|
||||
def installed() {
|
||||
log.trace "installed()"
|
||||
}
|
||||
|
||||
def uninstalled() {
|
||||
log.trace "uninstalled()"
|
||||
}
|
||||
|
||||
def configure() {
|
||||
/*
|
||||
def cmds =
|
||||
zigbee.configSetup("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}",
|
||||
"${TYPE_ENUM8}", 0, 3600, "{01}") +
|
||||
zigbee.configSetup("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}",
|
||||
"${TYPE_U8}", 600, 21600, "{01}")
|
||||
*/
|
||||
def zigbeeId = device.zigbeeId
|
||||
def cmds =
|
||||
[
|
||||
"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 1 ${CLUSTER_DOORLOCK} {$zigbeeId} {}", "delay 200",
|
||||
"zcl global send-me-a-report ${CLUSTER_DOORLOCK} ${DOORLOCK_ATTR_LOCKSTATE} ${TYPE_ENUM8} 0 3600 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 0x${device.endpointId}", "delay 200",
|
||||
|
||||
"zdo bind 0x${device.deviceNetworkId} 0x${device.endpointId} 1 ${CLUSTER_POWER} {$zigbeeId} {}", "delay 200",
|
||||
"zcl global send-me-a-report ${CLUSTER_POWER} ${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING} ${TYPE_U8} 600 21600 {01}", "delay 200",
|
||||
"send 0x${device.deviceNetworkId} 1 0x${device.endpointId}", "delay 200",
|
||||
]
|
||||
log.info "configure() --- cmds: $cmds"
|
||||
return cmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
def cmds =
|
||||
zigbee.refreshData("${CLUSTER_DOORLOCK}", "${DOORLOCK_ATTR_LOCKSTATE}") +
|
||||
zigbee.refreshData("${CLUSTER_POWER}", "${POWER_ATTR_BATTERY_PERCENTAGE_REMAINING}")
|
||||
log.info "refresh() --- cmds: $cmds"
|
||||
return cmds
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
log.trace "parse() --- description: $description"
|
||||
|
||||
Map map = [:]
|
||||
if (description?.startsWith('read attr -')) {
|
||||
map = parseReportAttributeMessage(description)
|
||||
}
|
||||
|
||||
log.debug "parse() --- Parse returned $map"
|
||||
def result = map ? createEvent(map) : null
|
||||
return result
|
||||
}
|
||||
|
||||
// Lock capability commands
|
||||
def lock() {
|
||||
//def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_LOCK_DOOR}", "{}")
|
||||
//log.info "lock() -- cmds: $cmds"
|
||||
//return cmds
|
||||
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_LOCK_DOOR} {}"
|
||||
}
|
||||
|
||||
def unlock() {
|
||||
//def cmds = zigbee.zigbeeCommand("${CLUSTER_DOORLOCK}", "${DOORLOCK_CMD_UNLOCK_DOOR}", "{}")
|
||||
//log.info "unlock() -- cmds: $cmds"
|
||||
//return cmds
|
||||
"st cmd 0x${device.deviceNetworkId} 0x${device.endpointId} ${CLUSTER_DOORLOCK} ${DOORLOCK_CMD_UNLOCK_DOOR} {}"
|
||||
}
|
||||
|
||||
// Private methods
|
||||
private Map parseReportAttributeMessage(String description) {
|
||||
log.trace "parseReportAttributeMessage() --- description: $description"
|
||||
|
||||
Map descMap = zigbee.parseDescriptionAsMap(description)
|
||||
|
||||
log.debug "parseReportAttributeMessage() --- descMap: $descMap"
|
||||
|
||||
Map resultMap = [:]
|
||||
if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) {
|
||||
resultMap.name = "battery"
|
||||
resultMap.value = Math.round(Integer.parseInt(descMap.value, 16) / 2)
|
||||
if (device.getDataValue("manufacturer") == "Yale") { //Handling issue with Yale locks incorrect battery reporting
|
||||
resultMap.value = Integer.parseInt(descMap.value, 16)
|
||||
}
|
||||
log.info "parseReportAttributeMessage() --- battery: ${resultMap.value}"
|
||||
}
|
||||
else if (descMap.clusterInt == CLUSTER_DOORLOCK && descMap.attrInt == DOORLOCK_ATTR_LOCKSTATE) {
|
||||
def value = Integer.parseInt(descMap.value, 16)
|
||||
resultMap.name = "lock"
|
||||
resultMap.putAll([0:["value":"unknown",
|
||||
"descriptionText":"Not fully locked"],
|
||||
1:["value":"locked"],
|
||||
2:["value":"unlocked"]].get(value,
|
||||
["value":"unknown",
|
||||
"descriptionText":"Unknown lock state"]))
|
||||
log.info "parseReportAttributeMessage() --- lock: ${resultMap.value}"
|
||||
}
|
||||
else {
|
||||
log.debug "parseReportAttributeMessage() --- ignoring attribute"
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* 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 RGBW ZigBee HA devices
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "ZigBee RGBW Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
capability "Actuator"
|
||||
capability "Color Control"
|
||||
capability "Color Temperature"
|
||||
capability "Configuration"
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Flex RGBW", deviceJoinName: "OSRAM LIGHTIFY LED FLEXIBLE STRIP RGBW"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW", deviceJoinName: "OSRAM LIGHTIFY LED FLEXIBLE STRIP RGBW"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 RGBW", deviceJoinName: "OSRAM LIGHTIFY LED A19 RGBW"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY BR RGBW", deviceJoinName: "OSRAM LIGHTIFY LED BR30 RGBW"
|
||||
fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY RT RGBW", deviceJoinName: "OSRAM LIGHTIFY LED RT 5/6 RGBW"
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
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.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
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.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()
|
||||
}
|
||||
|
||||
def off() {
|
||||
zigbee.off()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
zigbee.readAttribute(0x0006, 0x00) + 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)
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
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)
|
||||
}
|
||||
|
||||
def setColorTemperature(value) {
|
||||
zigbee.setColorTemperature(value)
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
zigbee.setLevel(value)
|
||||
}
|
||||
|
||||
def setColor(value){
|
||||
log.trace "setColor($value)"
|
||||
zigbee.on() + setHue(value.hue) + "delay 300" + 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") //payload-> sat value, transition time
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "ZigBee Switch Power", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Power Meter"
|
||||
capability "Sensor"
|
||||
capability "Switch"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B04"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702, 0B05", outClusters: "0003, 000A, 0019", manufacturer: "Jasco Products", model: "45853", deviceJoinName: "GE ZigBee Plug-In Switch"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702, 0B05", outClusters: "000A, 0019", manufacturer: "Jasco Products", model: "45856", deviceJoinName: "GE ZigBee In-Wall Switch"
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
tileAttribute ("power", key: "SECONDARY_CONTROL") {
|
||||
attributeState "power", label:'${currentValue} W'
|
||||
}
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
main "switch"
|
||||
details(["switch", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def resultMap = zigbee.getKnownDescription(description)
|
||||
if (resultMap) {
|
||||
log.info resultMap
|
||||
if (resultMap.type == "update") {
|
||||
log.info "$device updates: ${resultMap.value}"
|
||||
}
|
||||
else if (resultMap.type == "power") {
|
||||
def powerValue
|
||||
if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power
|
||||
powerValue = (resultMap.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration
|
||||
sendEvent(name: "power", value: powerValue)
|
||||
}
|
||||
}
|
||||
else {
|
||||
sendEvent(name: resultMap.type, value: resultMap.value)
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug zigbee.parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
zigbee.off()
|
||||
}
|
||||
|
||||
def on() {
|
||||
zigbee.on()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
zigbee.onOffRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
zigbee.onOffConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() + zigbee.onOffRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh()
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "ZigBee Switch", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Switch"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
// status messages
|
||||
status "on": "on/off: 1"
|
||||
status "off": "on/off: 0"
|
||||
|
||||
// reply messages
|
||||
reply "zcl on-off on": "on/off: 1"
|
||||
reply "zcl on-off off": "on/off: 0"
|
||||
}
|
||||
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
main "switch"
|
||||
details(["switch", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def resultMap = zigbee.getKnownDescription(description)
|
||||
if (resultMap) {
|
||||
log.info resultMap
|
||||
if (resultMap.type == "update") {
|
||||
log.info "$device updates: ${resultMap.value}"
|
||||
}
|
||||
else {
|
||||
sendEvent(name: resultMap.type, value: resultMap.value)
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug zigbee.parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
zigbee.off()
|
||||
}
|
||||
|
||||
def on() {
|
||||
zigbee.on()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
zigbee.onOffRefresh() + zigbee.onOffConfig()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
zigbee.onOffConfig() + zigbee.onOffRefresh()
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* ZigBee White Color Temperature Bulb
|
||||
*
|
||||
* Author: SmartThings
|
||||
* Date: 2015-09-22
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "ZigBee White Color Temperature Bulb", namespace: "smartthings", author: "SmartThings") {
|
||||
|
||||
capability "Actuator"
|
||||
capability "Color Temperature"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Switch"
|
||||
capability "Switch Level"
|
||||
|
||||
attribute "colorName", "string"
|
||||
command "setGenericName"
|
||||
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04", outClusters: "0019"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY BR Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Flood BR30 Tunable White"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY RT Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Recessed Kit RT 5/6 Tunable White"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 TW", deviceJoinName: "OSRAM LIGHTIFY LED Tunable White 60W"
|
||||
fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 Tunable White", deviceJoinName: "OSRAM LIGHTIFY LED Tunable White 60W"
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
|
||||
attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
|
||||
attributeState "level", action:"switch level.setLevel"
|
||||
}
|
||||
tileAttribute ("colorName", key: "SECONDARY_CONTROL") {
|
||||
attributeState "colorName", label:'${currentValue}'
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") {
|
||||
state "colorTemperature", action:"color temperature.setColorTemperature"
|
||||
}
|
||||
valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
|
||||
state "colorTemperature", label: '${currentValue} K'
|
||||
}
|
||||
|
||||
main(["switch"])
|
||||
details(["switch", "colorTempSliderControl", "colorTemp", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
log.debug "description is $description"
|
||||
|
||||
def finalResult = zigbee.getKnownDescription(description)
|
||||
if (finalResult) {
|
||||
log.info finalResult
|
||||
if (finalResult.type == "update") {
|
||||
log.info "$device updates: ${finalResult.value}"
|
||||
}
|
||||
else {
|
||||
sendEvent(name: finalResult.type, value: finalResult.value)
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.warn "DID NOT PARSE MESSAGE for description : $description"
|
||||
log.debug zigbee.parseDescriptionAsMap(description)
|
||||
}
|
||||
}
|
||||
|
||||
def off() {
|
||||
zigbee.off()
|
||||
}
|
||||
|
||||
def on() {
|
||||
zigbee.on()
|
||||
}
|
||||
|
||||
def setLevel(value) {
|
||||
zigbee.setLevel(value)
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig()
|
||||
}
|
||||
|
||||
def configure() {
|
||||
log.debug "Configuring Reporting and Bindings."
|
||||
zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.colorTemperatureConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh()
|
||||
}
|
||||
|
||||
def setColorTemperature(value) {
|
||||
setGenericName(value)
|
||||
zigbee.setColorTemperature(value)
|
||||
}
|
||||
|
||||
//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature
|
||||
def setGenericName(value){
|
||||
if (value != null) {
|
||||
def genericName = "White"
|
||||
if (value < 3300) {
|
||||
genericName = "Soft White"
|
||||
} else if (value < 4150) {
|
||||
genericName = "Moonlight"
|
||||
} else if (value <= 5000) {
|
||||
genericName = "Cool White"
|
||||
} else if (value >= 5000) {
|
||||
genericName = "Daylight"
|
||||
}
|
||||
sendEvent(name: "colorName", value: genericName)
|
||||
}
|
||||
}
|
||||
@@ -66,9 +66,20 @@ metadata {
|
||||
import physicalgraph.zwave.commands.doorlockv1.*
|
||||
import physicalgraph.zwave.commands.usercodev1.*
|
||||
|
||||
def updated() {
|
||||
try {
|
||||
if (!state.init) {
|
||||
state.init = true
|
||||
response(secureSequence([zwave.doorLockV1.doorLockOperationGet(), zwave.batteryV1.batteryGet()]))
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn "updated() threw $e"
|
||||
}
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
def result = null
|
||||
if (description.startsWith("Err")) {
|
||||
if (description.startsWith("Err 106")) {
|
||||
if (state.sec) {
|
||||
result = createEvent(descriptionText:description, displayed:false)
|
||||
} else {
|
||||
@@ -80,6 +91,8 @@ def parse(String description) {
|
||||
displayed: true,
|
||||
)
|
||||
}
|
||||
} else if (description == "updated") {
|
||||
return null
|
||||
} else {
|
||||
def cmd = zwave.parse(description, [ 0x98: 1, 0x72: 2, 0x85: 2, 0x86: 1 ])
|
||||
if (cmd) {
|
||||
@@ -286,7 +299,7 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) {
|
||||
}
|
||||
break
|
||||
case 167:
|
||||
if (!state.lastbatt || (new Date().time) - state.lastbatt > 12*60*60*1000) {
|
||||
if (!state.lastbatt || now() - state.lastbatt > 12*60*60*1000) {
|
||||
map = [ descriptionText: "$device.displayName: battery low", isStateChange: true ]
|
||||
result << response(secure(zwave.batteryV1.batteryGet()))
|
||||
} else {
|
||||
@@ -431,7 +444,7 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||
} else {
|
||||
map.value = cmd.batteryLevel
|
||||
}
|
||||
state.lastbatt = new Date().time
|
||||
state.lastbatt = now()
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
@@ -499,15 +512,14 @@ def refresh() {
|
||||
cmds << "delay 4200"
|
||||
cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format() // old Schlage locks use group 2 and don't secure the Association CC
|
||||
cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
|
||||
state.associationQuery = new Date().time
|
||||
} else if (new Date().time - state.associationQuery.toLong() > 9000) {
|
||||
log.debug "setting association"
|
||||
state.associationQuery = now()
|
||||
} else if (secondsPast(state.associationQuery, 9)) {
|
||||
cmds << "delay 6000"
|
||||
cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format()
|
||||
cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))
|
||||
cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format()
|
||||
cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
|
||||
state.associationQuery = new Date().time
|
||||
state.associationQuery = now()
|
||||
}
|
||||
log.debug "refresh sending ${cmds.inspect()}"
|
||||
cmds
|
||||
@@ -515,55 +527,22 @@ def refresh() {
|
||||
|
||||
def poll() {
|
||||
def cmds = []
|
||||
if (state.assoc != zwaveHubNodeId && secondsPast(state.associationQuery, 19 * 60)) {
|
||||
log.debug "setting association"
|
||||
cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format()
|
||||
cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))
|
||||
cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format()
|
||||
cmds << "delay 6000"
|
||||
cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
|
||||
cmds << "delay 6000"
|
||||
state.associationQuery = new Date().time
|
||||
} else {
|
||||
// Only check lock state if it changed recently or we haven't had an update in an hour
|
||||
def latest = device.currentState("lock")?.date?.time
|
||||
if (!latest || !secondsPast(latest, 6 * 60) || secondsPast(state.lastPoll, 55 * 60)) {
|
||||
cmds << secure(zwave.doorLockV1.doorLockOperationGet())
|
||||
state.lastPoll = (new Date()).time
|
||||
} else if (!state.MSR) {
|
||||
cmds << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
|
||||
} else if (!state.fw) {
|
||||
cmds << zwave.versionV1.versionGet().format()
|
||||
} else if (!state.codes) {
|
||||
state.pollCode = 1
|
||||
cmds << secure(zwave.userCodeV1.usersNumberGet())
|
||||
} else if (state.pollCode && state.pollCode <= state.codes) {
|
||||
cmds << requestCode(state.pollCode)
|
||||
} else if (!state.lastbatt || (new Date().time) - state.lastbatt > 53*60*60*1000) {
|
||||
cmds << secure(zwave.batteryV1.batteryGet())
|
||||
} else if (!state.enc) {
|
||||
encryptCodes()
|
||||
state.enc = 1
|
||||
}
|
||||
// Only check lock state if it changed recently or we haven't had an update in an hour
|
||||
def latest = device.currentState("lock")?.date?.time
|
||||
if (!latest || !secondsPast(latest, 6 * 60) || secondsPast(state.lastPoll, 55 * 60)) {
|
||||
cmds << secure(zwave.doorLockV1.doorLockOperationGet())
|
||||
state.lastPoll = now()
|
||||
} else if (!state.lastbatt || now() - state.lastbatt > 53*60*60*1000) {
|
||||
cmds << secure(zwave.batteryV1.batteryGet())
|
||||
state.lastbatt = now() //inside-214
|
||||
}
|
||||
log.debug "poll is sending ${cmds.inspect()}"
|
||||
device.activity()
|
||||
cmds ?: null
|
||||
}
|
||||
|
||||
private def encryptCodes() {
|
||||
def keys = new ArrayList(state.keySet().findAll { it.startsWith("code") })
|
||||
keys.each { key ->
|
||||
def match = (key =~ /^code(\d+)$/)
|
||||
if (match) try {
|
||||
def keynum = match[0][1].toInteger()
|
||||
if (keynum > 30 && !state[key]) {
|
||||
state.remove(key)
|
||||
} else if (state[key] && !state[key].startsWith("~")) {
|
||||
log.debug "encrypting $key: ${state[key].inspect()}"
|
||||
state[key] = encrypt(state[key])
|
||||
}
|
||||
} catch (java.lang.NumberFormatException e) { }
|
||||
if (cmds) {
|
||||
log.debug "poll is sending ${cmds.inspect()}"
|
||||
cmds
|
||||
} else {
|
||||
// workaround to keep polling from stopping due to lack of activity
|
||||
sendEvent(descriptionText: "skipping poll", isStateChange: true, displayed: false)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -672,7 +651,7 @@ private Boolean secondsPast(timestamp, seconds) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return (new Date().time - timestamp) > (seconds * 1000)
|
||||
return (now() - timestamp) > (seconds * 1000)
|
||||
}
|
||||
|
||||
private allCodesDeleted() {
|
||||
|
||||
@@ -115,6 +115,10 @@ def strobe() {
|
||||
]
|
||||
}
|
||||
|
||||
def both() {
|
||||
on()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "sending battery refresh command"
|
||||
zwave.batteryV1.batteryGet().format()
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Z-Wave Water Valve", namespace: "smartthings", author: "SmartThings") {
|
||||
capability "Actuator"
|
||||
capability "Valve"
|
||||
capability "Polling"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
|
||||
fingerprint deviceId: "0x1006", inClusters: "0x25"
|
||||
}
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
status "open": "command: 2503, payload: FF"
|
||||
status "close": "command: 2503, payload: 00"
|
||||
|
||||
// reply messages
|
||||
reply "2001FF,delay 100,2502": "command: 2503, payload: FF"
|
||||
reply "200100,delay 100,2502": "command: 2503, payload: 00"
|
||||
}
|
||||
|
||||
// tile definitions
|
||||
tiles(scale: 2) {
|
||||
multiAttributeTile(name:"valve", type: "generic", width: 6, height: 4, canChangeIcon: true){
|
||||
tileAttribute ("device.contact", key: "PRIMARY_CONTROL") {
|
||||
attributeState "open", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#53a7c0", nextState:"closing"
|
||||
attributeState "closed", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#e86d13", nextState:"opening"
|
||||
attributeState "opening", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#ffe71e"
|
||||
attributeState "closing", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#ffe71e"
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("refresh", "device.contact", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
main "valve"
|
||||
details(["valve","refresh"])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def updated() {
|
||||
response(refresh())
|
||||
}
|
||||
|
||||
def parse(String description) {
|
||||
log.trace "parse description : $description"
|
||||
def result = null
|
||||
def cmd = zwave.parse(description, [0x20: 1])
|
||||
if (cmd) {
|
||||
result = createEvent(zwaveEvent(cmd))
|
||||
}
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) {
|
||||
def value = cmd.value == 0xFF ? "open" : cmd.value == 0x00 ? "closed" : "unknown"
|
||||
[name: "contact", value: value, descriptionText: "$device.displayName valve is $value"]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { //TODO should show MSR when device is discovered
|
||||
log.debug "manufacturerId: ${cmd.manufacturerId}"
|
||||
log.debug "manufacturerName: ${cmd.manufacturerName}"
|
||||
log.debug "productId: ${cmd.productId}"
|
||||
log.debug "productTypeId: ${cmd.productTypeId}"
|
||||
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
|
||||
updateDataValue("MSR", msr)
|
||||
[descriptionText: "$device.displayName MSR: $msr", isStateChange: false]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) {
|
||||
[descriptionText: cmd.toString(), isStateChange: true, displayed: true]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
|
||||
def value = cmd.value == 0xFF ? "open" : cmd.value == 0x00 ? "closed" : "unknown"
|
||||
[name: "contact", value: value, descriptionText: "$device.displayName valve is $value"]
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
[:] // Handles all Z-Wave commands we aren't interested in
|
||||
}
|
||||
|
||||
def open() {
|
||||
delayBetween([
|
||||
zwave.basicV1.basicSet(value: 0xFF).format(),
|
||||
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||
],10000) //wait for a water valve to be completely opened
|
||||
}
|
||||
|
||||
def close() {
|
||||
delayBetween([
|
||||
zwave.basicV1.basicSet(value: 0x00).format(),
|
||||
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||
],10000) //wait for a water valve to be completely closed
|
||||
}
|
||||
|
||||
def poll() {
|
||||
zwave.switchBinaryV1.switchBinaryGet().format()
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "refresh() is called"
|
||||
def commands = [zwave.switchBinaryV1.switchBinaryGet().format()]
|
||||
if (getDataValue("MSR") == null) {
|
||||
commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
|
||||
}
|
||||
delayBetween(commands,100)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* Switch Too
|
||||
*
|
||||
* Copyright 2015 Bob Florian
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Switch Too", author: "Bob Florian") {
|
||||
capability "Switch"
|
||||
}
|
||||
|
||||
simulator {
|
||||
// TODO: define status and reply messages here
|
||||
}
|
||||
|
||||
tiles {
|
||||
// TODO: define your main and details tiles here
|
||||
}
|
||||
}
|
||||
|
||||
// parse events into attributes
|
||||
def parse(String description) {
|
||||
log.debug "Parsing '${description}'"
|
||||
// TODO: handle 'switch' attribute
|
||||
|
||||
}
|
||||
|
||||
// handle commands
|
||||
def on() {
|
||||
log.debug "Executing 'on'"
|
||||
// TODO: handle 'on' command
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "Executing 'off'"
|
||||
// TODO: handle 'off' command
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
metadata {
|
||||
definition (name: "Timevalve Smart", namespace: "timevalve.gaslock.t-08", author: "ruinnel") {
|
||||
capability "Valve"
|
||||
capability "Refresh"
|
||||
capability "Battery"
|
||||
capability "Temperature Measurement"
|
||||
|
||||
command "setRemaining"
|
||||
command "setTimeout"
|
||||
command "setTimeout10"
|
||||
command "setTimeout20"
|
||||
command "setTimeout30"
|
||||
command "setTimeout40"
|
||||
|
||||
command "remainingLevel"
|
||||
|
||||
attribute "remaining", "number"
|
||||
attribute "remainingText", "String"
|
||||
attribute "timeout", "number"
|
||||
|
||||
//raw desc : 0 0 0x1006 0 0 0 7 0x5E 0x86 0x72 0x5A 0x73 0x98 0x80
|
||||
//fingerprint deviceId:"0x1006", inClusters:"0x5E, 0x86, 0x72, 0x5A, 0x73, 0x98, 0x80"
|
||||
}
|
||||
|
||||
tiles (scale: 2) {
|
||||
multiAttributeTile(name:"statusTile", type:"generic", width:6, height:4) {
|
||||
tileAttribute("device.contact", key: "PRIMARY_CONTROL") {
|
||||
attributeState "open", label: '${name}', action: "close", icon:"st.contact.contact.open", backgroundColor:"#ffa81e"
|
||||
attributeState "closed", label:'${name}', action: "", icon:"st.contact.contact.closed", backgroundColor:"#79b821"
|
||||
}
|
||||
tileAttribute("device.remainingText", key: "SECONDARY_CONTROL") {
|
||||
attributeState "open", label: '${currentValue}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e"
|
||||
attributeState "closed", label:'', icon:"st.contact.contact.closed", backgroundColor:"#79b821"
|
||||
}
|
||||
}
|
||||
|
||||
standardTile("refreshTile", "command.refresh", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
|
||||
controlTile("remainingSliderTile", "device.remaining", "slider", inactiveLabel: false, range:"(0..590)", height: 2, width: 4) {
|
||||
state "level", action:"remainingLevel"
|
||||
}
|
||||
valueTile("setRemaining", "device.remainingText", inactiveLabel: false, decoration: "flat", height: 2, width: 2){
|
||||
state "remainingText", label:'${currentValue}\nRemaining'//, action: "setRemaining"//, icon: "st.Office.office6"
|
||||
}
|
||||
|
||||
standardTile("setTimeout10", "device.remaining", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'10Min', action: "setTimeout10", icon:"st.Health & Wellness.health7", defaultState: true
|
||||
state "10", label:'10Min', action: "setTimeout10", icon:"st.Office.office13"
|
||||
}
|
||||
standardTile("setTimeout20", "device.remaining", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'20Min', action: "setTimeout20", icon:"st.Health & Wellness.health7", defaultState: true
|
||||
state "20", label:'20Min', action: "setTimeout20", icon:"st.Office.office13"
|
||||
}
|
||||
standardTile("setTimeout30", "device.remaining", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'30Min', action: "setTimeout30", icon:"st.Health & Wellness.health7", defaultState: true
|
||||
state "30", label:'30Min', action: "setTimeout30", icon:"st.Office.office13"
|
||||
}
|
||||
standardTile("setTimeout40", "device.remaining", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:'40Min', action: "setTimeout40", icon:"st.Health & Wellness.health7", defaultState: true
|
||||
state "40", label:'40Min', action: "setTimeout40", icon:"st.Office.office13"
|
||||
}
|
||||
|
||||
valueTile("batteryTile", "device.battery", width: 2, height: 2, inactiveLabel: false, decoration: "flat") {
|
||||
state "battery", label:'${currentValue}% battery', unit:""
|
||||
}
|
||||
|
||||
main (["statusTile"])
|
||||
// details (["statusTile", "remainingSliderTile", "setRemaining", "setTimeout10", "setTimeout20", "batteryTile", "refreshTile", "setTimeout30", "setTimeout40"])
|
||||
// details (["statusTile", "batteryTile", "setRemaining", "refreshTile"])
|
||||
details (["statusTile", "batteryTile", "refreshTile"])
|
||||
}
|
||||
}
|
||||
|
||||
def parse(description) {
|
||||
// log.debug "parse - " + description
|
||||
def result = null
|
||||
if (description.startsWith("Err 106")) {
|
||||
state.sec = 0
|
||||
result = createEvent(descriptionText: description, isStateChange: true)
|
||||
} else if (description != "updated") {
|
||||
def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x70: 1, 0x71: 1, 0x98: 1])
|
||||
if (cmd) {
|
||||
log.debug "parsed cmd = " + cmd
|
||||
result = zwaveEvent(cmd)
|
||||
//log.debug("'$description' parsed to $result")
|
||||
} else {
|
||||
log.debug("Couldn't zwave.parse '$description'")
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 복호화 후 zwaveEvent() 호출
|
||||
def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
|
||||
//log.debug "SecurityMessageEncapsulation - " + cmd
|
||||
def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1, 0x70: 1, 0x71: 1, 0x98: 1])
|
||||
if (encapsulatedCommand) {
|
||||
state.sec = 1
|
||||
log.debug "encapsulatedCommand = " + encapsulatedCommand
|
||||
zwaveEvent(encapsulatedCommand)
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) {
|
||||
//log.debug "switch status - " + cmd.value
|
||||
createEvent(name:"contact", value: cmd.value ? "open" : "closed")
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) {
|
||||
def map = [ name: "battery", unit: "%" ]
|
||||
if (cmd.batteryLevel == 0xFF) { // Special value for low battery alert
|
||||
map.value = 1
|
||||
map.descriptionText = "${device.displayName} has a low battery"
|
||||
map.isStateChange = true
|
||||
} else {
|
||||
map.value = cmd.batteryLevel
|
||||
}
|
||||
|
||||
log.debug "battery - ${map.value}${map.unit}"
|
||||
// Store time of last battery update so we don't ask every wakeup, see WakeUpNotification handler
|
||||
state.lastbatt = new Date().time
|
||||
createEvent(map)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
//log.debug "zwaveEvent - ${device.displayName}: ${cmd}"
|
||||
createEvent(descriptionText: "${device.displayName}: ${cmd}")
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
|
||||
def result = []
|
||||
log.info "zwave.configurationV1.configurationGet - " + cmd
|
||||
def array = cmd.configurationValue
|
||||
def value = ( (array[0] * 0x1000000) + (array[1] * 0x10000) + (array[2] * 0x100) + array[3] ).intdiv(60)
|
||||
if (device.currentValue("contact") == "open") {
|
||||
value = ( (array[0] * 0x1000000) + (array[1] * 0x10000) + (array[2] * 0x100) + array[3] ).intdiv(60)
|
||||
} else {
|
||||
value = 0
|
||||
}
|
||||
|
||||
if (device.currentValue('contact') == 'open') {
|
||||
def hour = value.intdiv(60);
|
||||
def min = (value % 60).toString().padLeft(2, '0');
|
||||
def text = "${hour}:${min}M"
|
||||
|
||||
log.info "remain - " + text
|
||||
result.add( createEvent(name: "remaining", value: value, displayed: false, isStateChange: true) )
|
||||
result.add( createEvent(name: "remainingText", value: text, displayed: false, isStateChange: true) )
|
||||
} else {
|
||||
result.add( createEvent(name: "timeout", value: value, displayed: false, isStateChange: true) )
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) {
|
||||
def type = cmd.notificationType
|
||||
if (type == cmd.NOTIFICATION_TYPE_HEAT) {
|
||||
log.info "NotificationReport - ${type}"
|
||||
createEvent(name: "temperature", value: 999, unit: "C", descriptionText: "${device.displayName} is over heat!", displayed: true, isStateChange: true)
|
||||
}
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.alarmv1.AlarmReport cmd) {
|
||||
def type = cmd.alarmType
|
||||
def level = cmd.alarmLevel
|
||||
|
||||
log.info "AlarmReport - type : ${type}, level : ${level}"
|
||||
def msg = "${device.displayName} is over heat!"
|
||||
def result = createEvent(name: "temperature", value: 999, unit: "C", descriptionText: msg, displayed: true, isStateChange: true)
|
||||
if (sendPushMessage) {
|
||||
sendPushMessage(msg)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// remote open not allow
|
||||
def open() {}
|
||||
|
||||
def close() {
|
||||
// log.debug 'cmd - close()'
|
||||
commands([
|
||||
zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00),
|
||||
zwave.switchBinaryV1.switchBinaryGet()
|
||||
])
|
||||
}
|
||||
|
||||
def setTimeout10() { setTimeout(10) }
|
||||
def setTimeout20() { setTimeout(20) }
|
||||
def setTimeout30() { setTimeout(30) }
|
||||
def setTimeout40() { setTimeout(40) }
|
||||
|
||||
|
||||
def setTimeout(value) {
|
||||
// log.debug "setDefaultTime($value)"
|
||||
commands([
|
||||
zwave.configurationV1.configurationSet(parameterNumber: 0x01, size: 4, scaledConfigurationValue: value * 60),
|
||||
zwave.configurationV1.configurationGet(parameterNumber: 0x01)
|
||||
]);
|
||||
}
|
||||
|
||||
def remainingLevel(value) {
|
||||
// log.debug "remainingLevel($value)"
|
||||
def hour = value.intdiv(60);
|
||||
def min = (value % 60).toString().padLeft(2, '0');
|
||||
def text = "${hour}:${min}M"
|
||||
sendEvent(name: "remaining", value: value, displayed: false, isStateChange: true)
|
||||
sendEvent(name: "remainingText", value: text, displayed: false, isStateChange: true)
|
||||
}
|
||||
|
||||
def setRemaining() {
|
||||
def remaining = device.currentValue("remaining")
|
||||
// log.debug "setConfiguration() - remaining : $remaining"
|
||||
commands([
|
||||
zwave.configurationV1.configurationSet(parameterNumber: 0x03, size: 4, scaledConfigurationValue: remaining * 60),
|
||||
zwave.configurationV1.configurationGet(parameterNumber: 0x03)
|
||||
]);
|
||||
}
|
||||
|
||||
private command(physicalgraph.zwave.Command cmd) {
|
||||
if (state.sec != 0 && !(cmd instanceof physicalgraph.zwave.commands.batteryv1.BatteryGet)) {
|
||||
log.debug "cmd = " + cmd + ", encapsulation"
|
||||
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
|
||||
} else {
|
||||
log.debug "cmd = " + cmd + ", plain"
|
||||
cmd.format()
|
||||
}
|
||||
}
|
||||
|
||||
private commands(commands, delay=200) {
|
||||
delayBetween(commands.collect{ command(it) }, delay)
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
// log.debug 'cmd - refresh()'
|
||||
commands([
|
||||
zwave.batteryV1.batteryGet(),
|
||||
zwave.switchBinaryV1.switchBinaryGet(),
|
||||
zwave.configurationV1.configurationGet(parameterNumber: 0x01),
|
||||
zwave.configurationV1.configurationGet(parameterNumber: 0x03)
|
||||
], 400)
|
||||
}
|
||||
@@ -8,6 +8,7 @@ metadata {
|
||||
definition (name: "Zen Thermostat", namespace: "zenwithin", author: "ZenWithin") {
|
||||
capability "Actuator"
|
||||
capability "Thermostat"
|
||||
capability "Temperature Measurement"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
|
||||
@@ -20,7 +20,8 @@ definition(
|
||||
description: "Use this free SmartApp in conjunction with the ObyThing Music app for your Mac to control and automate music and more with iTunes and SmartThings.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "http://obycode.com/obything/ObyThingSTLogo.png",
|
||||
iconX2Url: "http://obycode.com/obything/ObyThingSTLogo@2x.png")
|
||||
iconX2Url: "http://obycode.com/obything/ObyThingSTLogo@2x.png",
|
||||
singleInstance: true)
|
||||
|
||||
|
||||
preferences {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Vinli Home Beta
|
||||
*
|
||||
* Copyright 2015 Daniel
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Vinli Home Connect",
|
||||
namespace: "com.vinli.smartthings",
|
||||
author: "Daniel",
|
||||
description: "Allows Vinli users to connect their car to SmartThings",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://d3azp77rte0gip.cloudfront.net/smartapps/baeb2e5d-ebd0-49fe-a4ec-e92417ae20bb/images/vinli_oauth_60.png",
|
||||
iconX2Url: "https://d3azp77rte0gip.cloudfront.net/smartapps/baeb2e5d-ebd0-49fe-a4ec-e92417ae20bb/images/vinli_oauth_120.png",
|
||||
iconX3Url: "https://d3azp77rte0gip.cloudfront.net/smartapps/baeb2e5d-ebd0-49fe-a4ec-e92417ae20bb/images/vinli_oauth_120.png",
|
||||
oauth: true)
|
||||
|
||||
preferences {
|
||||
section ("Allow external service to control these things...") {
|
||||
input "switches", "capability.switch", multiple: true, required: true
|
||||
input "locks", "capability.lock", multiple: true, required: true
|
||||
}
|
||||
}
|
||||
|
||||
mappings {
|
||||
|
||||
path("/devices") {
|
||||
action: [
|
||||
GET: "listAllDevices"
|
||||
]
|
||||
}
|
||||
|
||||
path("/switches") {
|
||||
action: [
|
||||
GET: "listSwitches"
|
||||
]
|
||||
}
|
||||
path("/switches/:command") {
|
||||
action: [
|
||||
PUT: "updateSwitches"
|
||||
]
|
||||
}
|
||||
path("/switches/:id/:command") {
|
||||
action: [
|
||||
PUT: "updateSwitch"
|
||||
]
|
||||
}
|
||||
path("/locks/:command") {
|
||||
action: [
|
||||
PUT: "updateLocks"
|
||||
]
|
||||
}
|
||||
path("/locks/:id/:command") {
|
||||
action: [
|
||||
PUT: "updateLock"
|
||||
]
|
||||
}
|
||||
|
||||
path("/devices/:id/:command") {
|
||||
action: [
|
||||
PUT: "commandDevice"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// returns a list of all devices
|
||||
def listAllDevices() {
|
||||
def resp = []
|
||||
switches.each {
|
||||
resp << [name: it.name, label: it.label, value: it.currentValue("switch"), type: "switch", id: it.id, hub: it.hub.name]
|
||||
}
|
||||
|
||||
locks.each {
|
||||
resp << [name: it.name, label: it.label, value: it.currentValue("lock"), type: "lock", id: it.id, hub: it.hub.name]
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// returns a list like
|
||||
// [[name: "kitchen lamp", value: "off"], [name: "bathroom", value: "on"]]
|
||||
def listSwitches() {
|
||||
def resp = []
|
||||
switches.each {
|
||||
resp << [name: it.displayName, value: it.currentValue("switch"), type: "switch", id: it.id]
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
void updateLocks() {
|
||||
// use the built-in request object to get the command parameter
|
||||
def command = params.command
|
||||
|
||||
if (command) {
|
||||
|
||||
// check that the switch supports the specified command
|
||||
// If not, return an error using httpError, providing a HTTP status code.
|
||||
locks.each {
|
||||
if (!it.hasCommand(command)) {
|
||||
httpError(501, "$command is not a valid command for all switches specified")
|
||||
}
|
||||
}
|
||||
|
||||
// all switches have the comand
|
||||
// execute the command on all switches
|
||||
// (note we can do this on the array - the command will be invoked on every element
|
||||
locks."$command"()
|
||||
}
|
||||
}
|
||||
|
||||
void updateLock() {
|
||||
def command = params.command
|
||||
|
||||
locks.each {
|
||||
if (!it.hasCommand(command)) {
|
||||
httpError(400, "$command is not a valid command for all lock specified")
|
||||
}
|
||||
|
||||
if (it.id == params.id) {
|
||||
it."$command"()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateSwitch() {
|
||||
def command = params.command
|
||||
|
||||
switches.each {
|
||||
if (!it.hasCommand(command)) {
|
||||
httpError(400, "$command is not a valid command for all switches specified")
|
||||
}
|
||||
|
||||
if (it.id == params.id) {
|
||||
it."$command"()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void commandDevice() {
|
||||
def command = params.command
|
||||
def devices = []
|
||||
|
||||
switches.each {
|
||||
devices << it
|
||||
}
|
||||
|
||||
locks.each {
|
||||
devices << it
|
||||
}
|
||||
|
||||
devices.each {
|
||||
if (it.id == params.id) {
|
||||
if (!it.hasCommand(command)) {
|
||||
httpError(400, "$command is not a valid command for specified device")
|
||||
}
|
||||
it."$command"()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateSwitches() {
|
||||
// use the built-in request object to get the command parameter
|
||||
def command = params.command
|
||||
|
||||
if (command) {
|
||||
|
||||
// check that the switch supports the specified command
|
||||
// If not, return an error using httpError, providing a HTTP status code.
|
||||
switches.each {
|
||||
if (!it.hasCommand(command)) {
|
||||
httpError(400, "$command is not a valid command for all switches specified")
|
||||
}
|
||||
}
|
||||
|
||||
// all switches have the comand
|
||||
// execute the command on all switches
|
||||
// (note we can do this on the array - the command will be invoked on every element
|
||||
switches."$command"()
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unsubscribe()
|
||||
}
|
||||
@@ -22,7 +22,8 @@ definition(
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png",
|
||||
oauth: true
|
||||
oauth: true,
|
||||
singleInstance: true
|
||||
){
|
||||
appSetting "clientId"
|
||||
appSetting "clientSecret"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Initial State Event Streamer
|
||||
*
|
||||
* Copyright 2015 David Sulpy
|
||||
* Copyright 2016 David Sulpy
|
||||
*
|
||||
* 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:
|
||||
@@ -12,7 +12,12 @@
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* SmartThings data is sent from this SmartApp to Initial State. This is event data only for
|
||||
* devices for which the user has authorized. Likewise, Initial State's services call this
|
||||
* SmartApp on the user's behalf to configure Initial State specific parameters. The ToS and
|
||||
* Privacy Policy for Initial State can be found here: https://www.initialstate.com/terms
|
||||
*/
|
||||
|
||||
definition(
|
||||
name: "Initial State Event Streamer",
|
||||
namespace: "initialstate.events",
|
||||
@@ -28,32 +33,31 @@ import groovy.json.JsonSlurper
|
||||
|
||||
preferences {
|
||||
section("Choose which devices to monitor...") {
|
||||
//input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false
|
||||
input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false
|
||||
input "alarms", "capability.alarm", title: "Alarms", multiple: true, required: false
|
||||
//input "batteries", "capability.battery", title: "Batteries", multiple: true, required: false
|
||||
//input "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false
|
||||
//input "buttons", "capability.button", title: "Buttons", multiple: true, required: false
|
||||
//input "cos", "capability.carbonMonoxideDetector", title: "Carbon Monoxide Detectors", multiple: true, required: false
|
||||
//input "colors", "capability.colorControl", title: "Color Controllers", multiple: true, required: false
|
||||
input "batteries", "capability.battery", title: "Batteries", multiple: true, required: false
|
||||
input "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false
|
||||
input "cos", "capability.carbonMonoxideDetector", title: "Carbon Monoxide Detectors", multiple: true, required: false
|
||||
input "colors", "capability.colorControl", title: "Color Controllers", multiple: true, required: false
|
||||
input "contacts", "capability.contactSensor", title: "Contact Sensors", multiple: true, required: false
|
||||
//input "doorsControllers", "capability.doorControl", title: "Door Controllers", multiple: true, required: false
|
||||
//input "energyMeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false
|
||||
//input "illuminances", "capability.illuminanceMeasurement", title: "Illuminance Meters", multiple: true, required: false
|
||||
input "doorsControllers", "capability.doorControl", title: "Door Controllers", multiple: true, required: false
|
||||
input "energyMeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false
|
||||
input "illuminances", "capability.illuminanceMeasurement", title: "Illuminance Meters", multiple: true, required: false
|
||||
input "locks", "capability.lock", title: "Locks", multiple: true, required: false
|
||||
input "motions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false
|
||||
//input "musicPlayers", "capability.musicPlayer", title: "Music Players", multiple: true, required: false
|
||||
//input "powerMeters", "capability.powerMeter", title: "Power Meters", multiple: true, required: false
|
||||
input "musicPlayers", "capability.musicPlayer", title: "Music Players", multiple: true, required: false
|
||||
input "powerMeters", "capability.powerMeter", title: "Power Meters", multiple: true, required: false
|
||||
input "presences", "capability.presenceSensor", title: "Presence Sensors", multiple: true, required: false
|
||||
input "humidities", "capability.relativeHumidityMeasurement", title: "Humidity Meters", multiple: true, required: false
|
||||
//input "relaySwitches", "capability.relaySwitch", title: "Relay Switches", multiple: true, required: false
|
||||
//input "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", multiple: true, required: false
|
||||
//input "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", multiple: true, required: false
|
||||
//input "peds", "capability.stepSensor", title: "Pedometers", multiple: true, required: false
|
||||
input "relaySwitches", "capability.relaySwitch", title: "Relay Switches", multiple: true, required: false
|
||||
input "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", multiple: true, required: false
|
||||
input "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", multiple: true, required: false
|
||||
input "peds", "capability.stepSensor", title: "Pedometers", multiple: true, required: false
|
||||
input "switches", "capability.switch", title: "Switches", multiple: true, required: false
|
||||
input "switchLevels", "capability.switchLevel", title: "Switch Levels", multiple: true, required: false
|
||||
input "temperatures", "capability.temperatureMeasurement", title: "Temperature Sensors", multiple: true, required: false
|
||||
input "thermostats", "capability.thermostat", title: "Thermostats", multiple: true, required: false
|
||||
//input "valves", "capability.valve", title: "Valves", multiple: true, required: false
|
||||
input "valves", "capability.valve", title: "Valves", multiple: true, required: false
|
||||
input "waterSensors", "capability.waterSensor", title: "Water Sensors", multiple: true, required: false
|
||||
}
|
||||
}
|
||||
@@ -73,78 +77,128 @@ mappings {
|
||||
}
|
||||
}
|
||||
|
||||
def getAccessKey() {
|
||||
log.trace "get access key"
|
||||
if (atomicState.accessKey == null) {
|
||||
httpError(404, "Access Key Not Found")
|
||||
} else {
|
||||
[
|
||||
accessKey: atomicState.accessKey
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def getBucketKey() {
|
||||
log.trace "get bucket key"
|
||||
if (atomicState.bucketKey == null) {
|
||||
httpError(404, "Bucket key Not Found")
|
||||
} else {
|
||||
[
|
||||
bucketKey: atomicState.bucketKey,
|
||||
bucketName: atomicState.bucketName
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def setBucketKey() {
|
||||
log.trace "set bucket key"
|
||||
def newBucketKey = request.JSON?.bucketKey
|
||||
def newBucketName = request.JSON?.bucketName
|
||||
|
||||
log.debug "bucket name: $newBucketName"
|
||||
log.debug "bucket key: $newBucketKey"
|
||||
|
||||
if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) {
|
||||
atomicState.bucketKey = "$newBucketKey"
|
||||
atomicState.bucketName = "$newBucketName"
|
||||
atomicState.isBucketCreated = false
|
||||
}
|
||||
|
||||
tryCreateBucket()
|
||||
}
|
||||
|
||||
def setAccessKey() {
|
||||
log.trace "set access key"
|
||||
def newAccessKey = request.JSON?.accessKey
|
||||
def newGrokerSubdomain = request.JSON?.grokerSubdomain
|
||||
|
||||
if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) {
|
||||
atomicState.grokerSubdomain = "$newGrokerSubdomain"
|
||||
atomicState.isBucketCreated = false
|
||||
}
|
||||
|
||||
if (newAccessKey && newAccessKey != atomicState.accessKey) {
|
||||
atomicState.accessKey = "$newAccessKey"
|
||||
atomicState.isBucketCreated = false
|
||||
}
|
||||
}
|
||||
|
||||
def subscribeToEvents() {
|
||||
/*if (accelerometers != null) {
|
||||
if (accelerometers != null) {
|
||||
subscribe(accelerometers, "acceleration", genericHandler)
|
||||
}*/
|
||||
}
|
||||
if (alarms != null) {
|
||||
subscribe(alarms, "alarm", genericHandler)
|
||||
}
|
||||
/*if (batteries != null) {
|
||||
if (batteries != null) {
|
||||
subscribe(batteries, "battery", genericHandler)
|
||||
}*/
|
||||
/*if (beacons != null) {
|
||||
}
|
||||
if (beacons != null) {
|
||||
subscribe(beacons, "presence", genericHandler)
|
||||
}*/
|
||||
/*
|
||||
if (buttons != null) {
|
||||
subscribe(buttons, "button", genericHandler)
|
||||
}*/
|
||||
/*if (cos != null) {
|
||||
}
|
||||
|
||||
if (cos != null) {
|
||||
subscribe(cos, "carbonMonoxide", genericHandler)
|
||||
}*/
|
||||
/*if (colors != null) {
|
||||
}
|
||||
if (colors != null) {
|
||||
subscribe(colors, "hue", genericHandler)
|
||||
subscribe(colors, "saturation", genericHandler)
|
||||
subscribe(colors, "color", genericHandler)
|
||||
}*/
|
||||
}
|
||||
if (contacts != null) {
|
||||
subscribe(contacts, "contact", genericHandler)
|
||||
}
|
||||
/*if (doorsControllers != null) {
|
||||
subscribe(doorsControllers, "door", genericHandler)
|
||||
}*/
|
||||
/*if (energyMeters != null) {
|
||||
if (energyMeters != null) {
|
||||
subscribe(energyMeters, "energy", genericHandler)
|
||||
}*/
|
||||
/*if (illuminances != null) {
|
||||
}
|
||||
if (illuminances != null) {
|
||||
subscribe(illuminances, "illuminance", genericHandler)
|
||||
}*/
|
||||
}
|
||||
if (locks != null) {
|
||||
subscribe(locks, "lock", genericHandler)
|
||||
}
|
||||
if (motions != null) {
|
||||
subscribe(motions, "motion", genericHandler)
|
||||
}
|
||||
/*if (musicPlayers != null) {
|
||||
if (musicPlayers != null) {
|
||||
subscribe(musicPlayers, "status", genericHandler)
|
||||
subscribe(musicPlayers, "level", genericHandler)
|
||||
subscribe(musicPlayers, "trackDescription", genericHandler)
|
||||
subscribe(musicPlayers, "trackData", genericHandler)
|
||||
subscribe(musicPlayers, "mute", genericHandler)
|
||||
}*/
|
||||
/*if (powerMeters != null) {
|
||||
}
|
||||
if (powerMeters != null) {
|
||||
subscribe(powerMeters, "power", genericHandler)
|
||||
}*/
|
||||
}
|
||||
if (presences != null) {
|
||||
subscribe(presences, "presence", genericHandler)
|
||||
}
|
||||
if (humidities != null) {
|
||||
subscribe(humidities, "humidity", genericHandler)
|
||||
}
|
||||
/*if (relaySwitches != null) {
|
||||
if (relaySwitches != null) {
|
||||
subscribe(relaySwitches, "switch", genericHandler)
|
||||
}*/
|
||||
/*if (sleepSensors != null) {
|
||||
}
|
||||
if (sleepSensors != null) {
|
||||
subscribe(sleepSensors, "sleeping", genericHandler)
|
||||
}*/
|
||||
/*if (smokeDetectors != null) {
|
||||
}
|
||||
if (smokeDetectors != null) {
|
||||
subscribe(smokeDetectors, "smoke", genericHandler)
|
||||
}*/
|
||||
/*if (peds != null) {
|
||||
}
|
||||
if (peds != null) {
|
||||
subscribe(peds, "steps", genericHandler)
|
||||
subscribe(peds, "goal", genericHandler)
|
||||
}*/
|
||||
}
|
||||
if (switches != null) {
|
||||
subscribe(switches, "switch", genericHandler)
|
||||
}
|
||||
@@ -163,92 +217,75 @@ def subscribeToEvents() {
|
||||
subscribe(thermostats, "thermostatFanMode", genericHandler)
|
||||
subscribe(thermostats, "thermostatOperatingState", genericHandler)
|
||||
}
|
||||
/*if (valves != null) {
|
||||
if (valves != null) {
|
||||
subscribe(valves, "contact", genericHandler)
|
||||
}*/
|
||||
}
|
||||
if (waterSensors != null) {
|
||||
subscribe(waterSensors, "water", genericHandler)
|
||||
}
|
||||
}
|
||||
|
||||
def getAccessKey() {
|
||||
log.trace "get access key"
|
||||
if (state.accessKey == null) {
|
||||
httpError(404, "Access Key Not Found")
|
||||
} else {
|
||||
[
|
||||
accessKey: state.accessKey
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def getBucketKey() {
|
||||
log.trace "get bucket key"
|
||||
if (state.bucketKey == null) {
|
||||
httpError(404, "Bucket key Not Found")
|
||||
} else {
|
||||
[
|
||||
bucketKey: state.bucketKey,
|
||||
bucketName: state.bucketName
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def setBucketKey() {
|
||||
log.trace "set bucket key"
|
||||
def newBucketKey = request.JSON?.bucketKey
|
||||
def newBucketName = request.JSON?.bucketName
|
||||
|
||||
log.debug "bucket name: $newBucketName"
|
||||
log.debug "bucket key: $newBucketKey"
|
||||
|
||||
if (newBucketKey && (newBucketKey != state.bucketKey || newBucketName != state.bucketName)) {
|
||||
state.bucketKey = "$newBucketKey"
|
||||
state.bucketName = "$newBucketName"
|
||||
state.isBucketCreated = false
|
||||
}
|
||||
}
|
||||
|
||||
def setAccessKey() {
|
||||
log.trace "set access key"
|
||||
def newAccessKey = request.JSON?.accessKey
|
||||
|
||||
if (newAccessKey && newAccessKey != state.accessKey) {
|
||||
state.accessKey = "$newAccessKey"
|
||||
state.isBucketCreated = false
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
atomicState.version = "1.1.0"
|
||||
|
||||
atomicState.isBucketCreated = false
|
||||
atomicState.grokerSubdomain = "groker"
|
||||
|
||||
subscribeToEvents()
|
||||
|
||||
state.isBucketCreated = false
|
||||
atomicState.isBucketCreated = false
|
||||
atomicState.grokerSubdomain = "groker"
|
||||
|
||||
log.debug "installed (version $atomicState.version)"
|
||||
}
|
||||
|
||||
def updated() {
|
||||
atomicState.version = "1.1.0"
|
||||
unsubscribe()
|
||||
|
||||
if (state.bucketKey != null && state.accessKey != null) {
|
||||
state.isBucketCreated = false
|
||||
if (atomicState.bucketKey != null && atomicState.accessKey != null) {
|
||||
atomicState.isBucketCreated = false
|
||||
}
|
||||
|
||||
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
|
||||
atomicState.grokerSubdomain = "groker"
|
||||
}
|
||||
|
||||
subscribeToEvents()
|
||||
|
||||
log.debug "updated (version $atomicState.version)"
|
||||
}
|
||||
|
||||
def createBucket() {
|
||||
def uninstalled() {
|
||||
log.debug "uninstalled (version $atomicState.version)"
|
||||
}
|
||||
|
||||
if (!state.bucketName) {
|
||||
state.bucketName = state.bucketKey
|
||||
def tryCreateBucket() {
|
||||
|
||||
// can't ship events if there is no grokerSubdomain
|
||||
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
|
||||
log.error "streaming url is currently null"
|
||||
return
|
||||
}
|
||||
|
||||
// if the bucket has already been created, no need to continue
|
||||
if (atomicState.isBucketCreated) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!atomicState.bucketName) {
|
||||
atomicState.bucketName = atomicState.bucketKey
|
||||
}
|
||||
def bucketName = "${state.bucketName}"
|
||||
def bucketKey = "${state.bucketKey}"
|
||||
def accessKey = "${state.accessKey}"
|
||||
if (!atomicState.accessKey) {
|
||||
return
|
||||
}
|
||||
def bucketName = "${atomicState.bucketName}"
|
||||
def bucketKey = "${atomicState.bucketKey}"
|
||||
def accessKey = "${atomicState.accessKey}"
|
||||
|
||||
def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}")
|
||||
|
||||
def bucketCreatePost = [
|
||||
uri: 'https://groker.initialstate.com/api/buckets',
|
||||
uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/buckets",
|
||||
headers: [
|
||||
"Content-Type": "application/json",
|
||||
"X-IS-AccessKey": accessKey
|
||||
@@ -258,10 +295,20 @@ def createBucket() {
|
||||
|
||||
log.debug bucketCreatePost
|
||||
|
||||
httpPostJson(bucketCreatePost) {
|
||||
log.debug "bucket posted"
|
||||
state.isBucketCreated = true
|
||||
try {
|
||||
// Create a bucket on Initial State so the data has a logical grouping
|
||||
httpPostJson(bucketCreatePost) { resp ->
|
||||
log.debug "bucket posted"
|
||||
if (resp.status >= 400) {
|
||||
log.error "bucket not created successfully"
|
||||
} else {
|
||||
atomicState.isBucketCreated = true
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log.error "bucket creation error: $e"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def genericHandler(evt) {
|
||||
@@ -273,33 +320,57 @@ def genericHandler(evt) {
|
||||
}
|
||||
def value = "$evt.value"
|
||||
|
||||
tryCreateBucket()
|
||||
|
||||
eventHandler(key, value)
|
||||
}
|
||||
|
||||
def eventHandler(name, value) {
|
||||
def epoch = now() / 1000
|
||||
|
||||
if (state.accessKey == null || state.bucketKey == null) {
|
||||
def event = new JsonSlurper().parseText("{\"key\": \"$name\", \"value\": \"$value\", \"epoch\": \"$epoch\"}")
|
||||
|
||||
tryShipEvents(event)
|
||||
|
||||
log.debug "Shipped Event: " + event
|
||||
}
|
||||
|
||||
def tryShipEvents(event) {
|
||||
|
||||
def grokerSubdomain = atomicState.grokerSubdomain
|
||||
// can't ship events if there is no grokerSubdomain
|
||||
if (grokerSubdomain == null || grokerSubdomain == "") {
|
||||
log.error "streaming url is currently null"
|
||||
return
|
||||
}
|
||||
def accessKey = atomicState.accessKey
|
||||
def bucketKey = atomicState.bucketKey
|
||||
// can't ship if access key and bucket key are null, so finish trying
|
||||
if (accessKey == null || bucketKey == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!state.isBucketCreated) {
|
||||
createBucket()
|
||||
}
|
||||
|
||||
def eventBody = new JsonSlurper().parseText("[{\"key\": \"$name\", \"value\": \"$value\"}]")
|
||||
def eventPost = [
|
||||
uri: 'https://groker.initialstate.com/api/events',
|
||||
uri: "https://${grokerSubdomain}.initialstate.com/api/events",
|
||||
headers: [
|
||||
"Content-Type": "application/json",
|
||||
"X-IS-BucketKey": "${state.bucketKey}",
|
||||
"X-IS-AccessKey": "${state.accessKey}"
|
||||
"X-IS-BucketKey": "${bucketKey}",
|
||||
"X-IS-AccessKey": "${accessKey}",
|
||||
"Accept-Version": "0.0.2"
|
||||
],
|
||||
body: eventBody
|
||||
body: event
|
||||
]
|
||||
|
||||
log.debug eventPost
|
||||
|
||||
httpPostJson(eventPost) {
|
||||
log.debug "event data posted"
|
||||
try {
|
||||
// post the events to initial state
|
||||
httpPostJson(eventPost) { resp ->
|
||||
log.debug "shipped events and got ${resp.status}"
|
||||
if (resp.status >= 400) {
|
||||
log.error "shipping failed... ${resp.data}"
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log.error "shipping events failed: $e"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,11 +13,11 @@ definition(
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up@2x.png",
|
||||
oauth: true,
|
||||
usePreferencesForAuthorization: false
|
||||
usePreferencesForAuthorization: false,
|
||||
singleInstance: true
|
||||
) {
|
||||
appSetting "clientId"
|
||||
appSetting "clientSecret"
|
||||
appSetting "serverUrl"
|
||||
}
|
||||
|
||||
preferences {
|
||||
@@ -28,16 +28,13 @@ mappings {
|
||||
path("/receivedToken") { action: [ POST: "receivedToken", GET: "receivedToken"] }
|
||||
path("/receiveToken") { action: [ POST: "receiveToken", GET: "receiveToken"] }
|
||||
path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] }
|
||||
path("/oauth/initialize") {action: [GET: "oauthInitUrl"]}
|
||||
path("/oauth/callback") { action: [ GET: "callback" ] }
|
||||
}
|
||||
|
||||
def getSmartThingsClientId() {
|
||||
return appSettings.clientId
|
||||
}
|
||||
|
||||
def getSmartThingsClientSecret() {
|
||||
return appSettings.clientSecret
|
||||
}
|
||||
def getServerUrl() { return "https://graph.api.smartthings.com" }
|
||||
def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" }
|
||||
def buildRedirectUrl(page) { return buildActionUrl(page) }
|
||||
|
||||
def callback() {
|
||||
def redirectUrl = null
|
||||
@@ -63,9 +60,8 @@ def callback() {
|
||||
// SmartThings code, which we ignore, as we don't need to exchange for an access token.
|
||||
// Instead, go initiate the Jawbone OAuth flow.
|
||||
log.debug "Executing callback redirect to auth page"
|
||||
def stcid = getSmartThingsClientId()
|
||||
state.oauthInitState = UUID.randomUUID().toString()
|
||||
def oauthParams = [response_type: "code", client_id: stcid, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback"]
|
||||
def oauthParams = [response_type: "code", client_id: appSettings.clientId, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback"]
|
||||
redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}")
|
||||
}
|
||||
} else {
|
||||
@@ -84,10 +80,11 @@ def authPage() {
|
||||
createAccessToken()
|
||||
}
|
||||
description = "Click to enter Jawbone Credentials"
|
||||
def redirectUrl = oauthInitUrl()
|
||||
// log.debug "RedirectURL = ${redirectUrl}"
|
||||
return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install:false) {
|
||||
section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", description:description }
|
||||
def redirectUrl = buildRedirectUrl
|
||||
log.debug "RedirectURL = ${redirectUrl}"
|
||||
def donebutton= state.JawboneAccessToken != null
|
||||
return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install: donebutton) {
|
||||
section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", state: hast ,description:description }
|
||||
}
|
||||
} else {
|
||||
description = "Jawbone Credentials Already Entered."
|
||||
@@ -99,17 +96,14 @@ def authPage() {
|
||||
|
||||
def oauthInitUrl() {
|
||||
log.debug "oauthInitUrl"
|
||||
def stcid = getSmartThingsClientId()
|
||||
state.oauthInitState = UUID.randomUUID().toString()
|
||||
def oauthParams = [ response_type: "code", client_id: stcid, scope: "move_read sleep_read", redirect_uri: buildRedirectUrl("receiveToken") ]
|
||||
return "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}"
|
||||
def oauthParams = [ response_type: "code", client_id: appSettings.clientId, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback" ]
|
||||
redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}")
|
||||
}
|
||||
|
||||
def receiveToken(redirectUrl = null) {
|
||||
log.debug "receiveToken"
|
||||
def stcid = getSmartThingsClientId()
|
||||
def oauthClientSecret = getSmartThingsClientSecret()
|
||||
def oauthParams = [ client_id: stcid, client_secret: oauthClientSecret, grant_type: "authorization_code", code: params.code ]
|
||||
def oauthParams = [ client_id: appSettings.clientId, client_secret: appSettings.clientSecret, grant_type: "authorization_code", code: params.code ]
|
||||
def params = [
|
||||
uri: "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}",
|
||||
]
|
||||
@@ -231,18 +225,10 @@ String toQueryString(Map m) {
|
||||
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
||||
}
|
||||
|
||||
def getServerUrl() { return appSettings.serverUrl ?: "https://graph.api.smartthings.com" }
|
||||
|
||||
def buildRedirectUrl(page) {
|
||||
// log.debug "buildRedirectUrl"
|
||||
// /api/token/:st_token/smartapps/installations/:id/something
|
||||
return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}"
|
||||
}
|
||||
|
||||
def validateCurrentToken() {
|
||||
log.debug "validateCurrentToken"
|
||||
def url = "https://jawbone.com/nudge/api/v.1.1/users/@me/refreshToken"
|
||||
def requestBody = "secret=${getSmartThingsClientSecret()}"
|
||||
def requestBody = "secret=${appSettings.clientSecret}"
|
||||
|
||||
try {
|
||||
httpPost(uri: url, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], body: requestBody) {response ->
|
||||
@@ -256,9 +242,7 @@ def validateCurrentToken() {
|
||||
if (e.statusCode == 401) { // token is expired
|
||||
log.debug "Access token is expired"
|
||||
if (state.refreshToken) { // if we have this we are okay
|
||||
def stcid = getSmartThingsClientId()
|
||||
def oauthClientSecret = getSmartThingsClientSecret()
|
||||
def oauthParams = [client_id: stcid, client_secret: oauthClientSecret, grant_type: "refresh_token", refresh_token: state.refreshToken]
|
||||
def oauthParams = [client_id: appSettings.clientId, client_secret: appSettings.clientSecret, grant_type: "refresh_token", refresh_token: state.refreshToken]
|
||||
def tokenUrl = "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}"
|
||||
def params = [
|
||||
uri: tokenUrl
|
||||
@@ -287,9 +271,10 @@ def validateCurrentToken() {
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
def hookUrl = "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback"
|
||||
def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl"
|
||||
log.debug "Callback URL: $webhook"
|
||||
log.debug "Callback URL - Webhook"
|
||||
def localServerUrl = getApiServerUrl()
|
||||
def hookUrl = "${localServerUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback"
|
||||
def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl"
|
||||
httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ])
|
||||
}
|
||||
|
||||
@@ -327,7 +312,6 @@ def setup() {
|
||||
}
|
||||
|
||||
def installed() {
|
||||
enableCallback()
|
||||
|
||||
if (!state.accessToken) {
|
||||
log.debug "About to create access token"
|
||||
@@ -340,7 +324,6 @@ def installed() {
|
||||
}
|
||||
|
||||
def updated() {
|
||||
enableCallback()
|
||||
|
||||
if (!state.accessToken) {
|
||||
log.debug "About to create access token"
|
||||
@@ -499,4 +482,4 @@ def hookEventHandler() {
|
||||
|
||||
def html = """{"code":200,"message":"OK"}"""
|
||||
render contentType: 'application/json', data: html
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ preferences{
|
||||
input "lock1", "capability.lock", required: true
|
||||
}
|
||||
section("Select the door contact sensor:") {
|
||||
input "contact", "capability.contactSensor", required: true
|
||||
input "contact", "capability.contactSensor", required: true
|
||||
}
|
||||
section("Automatically lock the door when closed...") {
|
||||
input "minutesLater", "number", title: "Delay (in minutes):", required: true
|
||||
@@ -22,9 +22,10 @@ preferences{
|
||||
input "secondsLater", "number", title: "Delay (in seconds):", required: true
|
||||
}
|
||||
section( "Notifications" ) {
|
||||
input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes", "No"]], required: false
|
||||
input "phoneNumber", "phone", title: "Enter phone number to send text notification.", required: false
|
||||
}
|
||||
input("recipients", "contact", title: "Send notifications to", required: false) {
|
||||
input "phoneNumber", "phone", title: "Warn with text message (optional)", description: "Phone Number", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed(){
|
||||
@@ -42,55 +43,73 @@ def initialize(){
|
||||
subscribe(lock1, "lock", doorHandler, [filterEvents: false])
|
||||
subscribe(lock1, "unlock", doorHandler, [filterEvents: false])
|
||||
subscribe(contact, "contact.open", doorHandler)
|
||||
subscribe(contact, "contact.closed", doorHandler)
|
||||
subscribe(contact, "contact.closed", doorHandler)
|
||||
}
|
||||
|
||||
def lockDoor(){
|
||||
log.debug "Locking the door."
|
||||
lock1.lock()
|
||||
log.debug ( "Sending Push Notification..." )
|
||||
if ( sendPushMessage != "No" ) sendPush( "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!" )
|
||||
log.debug("Sending text message...")
|
||||
if ( phoneNumber != "0" ) sendSms( phoneNumber, "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!" )
|
||||
if(location.contactBookEnabled) {
|
||||
if ( recipients ) {
|
||||
log.debug ( "Sending Push Notification..." )
|
||||
sendNotificationToContacts( "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!", recipients)
|
||||
}
|
||||
}
|
||||
if (phoneNumber) {
|
||||
log.debug("Sending text message...")
|
||||
sendSms( phoneNumber, "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!")
|
||||
}
|
||||
}
|
||||
|
||||
def unlockDoor(){
|
||||
log.debug "Unlocking the door."
|
||||
lock1.unlock()
|
||||
log.debug ( "Sending Push Notification..." )
|
||||
if ( sendPushMessage != "No" ) sendPush( "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!" )
|
||||
log.debug("Sending text message...")
|
||||
if ( phoneNumber != "0" ) sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!" )
|
||||
if(location.contactBookEnabled) {
|
||||
if ( recipients ) {
|
||||
log.debug ( "Sending Push Notification..." )
|
||||
sendNotificationToContacts( "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!", recipients)
|
||||
}
|
||||
}
|
||||
if ( phoneNumber ) {
|
||||
log.debug("Sending text message...")
|
||||
sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!")
|
||||
}
|
||||
}
|
||||
|
||||
def doorHandler(evt){
|
||||
if ((contact.latestValue("contact") == "open") && (evt.value == "locked")) { // If the door is open and a person locks the door then...
|
||||
def delay = (secondsLater) // runIn uses seconds
|
||||
runIn( delay, unlockDoor ) // ...schedule (in minutes) to unlock... We don't want the door to be closed while the lock is engaged.
|
||||
//def delay = (secondsLater) // runIn uses seconds
|
||||
runIn( secondsLater, unlockDoor ) // ...schedule (in minutes) to unlock... We don't want the door to be closed while the lock is engaged.
|
||||
}
|
||||
else if ((contact.latestValue("contact") == "open") && (evt.value == "unlocked")) { // If the door is open and a person unlocks it then...
|
||||
unschedule( unlockDoor ) // ...we don't need to unlock it later.
|
||||
}
|
||||
}
|
||||
else if ((contact.latestValue("contact") == "closed") && (evt.value == "locked")) { // If the door is closed and a person manually locks it then...
|
||||
unschedule( lockDoor ) // ...we don't need to lock it later.
|
||||
}
|
||||
else if ((contact.latestValue("contact") == "closed") && (evt.value == "unlocked")) { // If the door is closed and a person unlocks it then...
|
||||
def delay = (minutesLater * 60) // runIn uses seconds
|
||||
runIn( delay, lockDoor ) // ...schedule (in minutes) to lock.
|
||||
//def delay = (minutesLater * 60) // runIn uses seconds
|
||||
runIn( (minutesLater * 60), lockDoor ) // ...schedule (in minutes) to lock.
|
||||
}
|
||||
else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "open")) { // If a person opens an unlocked door...
|
||||
unschedule( lockDoor ) // ...we don't need to lock it later.
|
||||
}
|
||||
else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "closed")) { // If a person closes an unlocked door...
|
||||
def delay = (minutesLater * 60) // runIn uses seconds
|
||||
runIn( delay, lockDoor ) // ...schedule (in minutes) to lock.
|
||||
}
|
||||
//def delay = (minutesLater * 60) // runIn uses seconds
|
||||
runIn( (minutesLater * 60), lockDoor ) // ...schedule (in minutes) to lock.
|
||||
}
|
||||
else { //Opening or Closing door when locked (in case you have a handle lock)
|
||||
log.debug "Unlocking the door."
|
||||
lock1.unlock()
|
||||
log.debug ( "Sending Push Notification..." )
|
||||
if ( sendPushMessage != "No" ) sendPush( "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!" )
|
||||
log.debug("Sending text message...")
|
||||
if ( phoneNumber != "0" ) sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!" )
|
||||
}
|
||||
log.debug "Unlocking the door."
|
||||
lock1.unlock()
|
||||
if(location.contactBookEnabled) {
|
||||
if ( recipients ) {
|
||||
log.debug ( "Sending Push Notification..." )
|
||||
sendNotificationToContacts( "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!", recipients)
|
||||
}
|
||||
}
|
||||
if ( phoneNumber ) {
|
||||
log.debug("Sending text message...")
|
||||
sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* Lights On When Door Open After Sundown
|
||||
*
|
||||
* Based on "Turn It On When It Opens" by SmartThings
|
||||
*
|
||||
* Author: Aaron Crocco
|
||||
*/
|
||||
preferences {
|
||||
section("When the door opens..."){
|
||||
input "contact1", "capability.contactSensor", title: "Where?"
|
||||
}
|
||||
section("Turn on these lights..."){
|
||||
input "switches", "capability.switch", multiple: true
|
||||
}
|
||||
section("and change mode to...") {
|
||||
input "HomeAfterDarkMode", "mode", title: "Mode?"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def installed()
|
||||
{
|
||||
subscribe(contact1, "contact.open", contactOpenHandler)
|
||||
}
|
||||
|
||||
def updated()
|
||||
{
|
||||
unsubscribe()
|
||||
subscribe(contact1, "contact.open", contactOpenHandler)
|
||||
}
|
||||
|
||||
def contactOpenHandler(evt) {
|
||||
log.debug "$evt.value: $evt, $settings"
|
||||
|
||||
//Check current time to see if it's after sundown.
|
||||
def s = getSunriseAndSunset(zipCode: zipCode, sunriseOffset: sunriseOffset, sunsetOffset: sunsetOffset)
|
||||
def now = new Date()
|
||||
def setTime = s.sunset
|
||||
log.debug "Sunset is at $setTime. Current time is $now"
|
||||
|
||||
|
||||
if (setTime.before(now)) { //Executes only if it's after sundown.
|
||||
|
||||
log.trace "Turning on switches: $switches"
|
||||
switches.on()
|
||||
log.trace "Changing house mode to $HomeAfterDarkMode"
|
||||
setLocationMode(HomeAfterDarkMode)
|
||||
sendPush("Welcome home! Changing mode to $HomeAfterDarkMode.")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ definition(
|
||||
iconUrl: "http://i.imgur.com/HU0ANBp.png",
|
||||
iconX2Url: "http://i.imgur.com/HU0ANBp.png",
|
||||
iconX3Url: "http://i.imgur.com/HU0ANBp.png",
|
||||
oauth: true)
|
||||
oauth: true,
|
||||
singleInstance: true)
|
||||
|
||||
|
||||
preferences {
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
/**
|
||||
* Required PlantLink Connector
|
||||
* This SmartApp forwards the raw data of the deviceType to myplantlink.com
|
||||
* and returns it back to your device after calculating soil and plant type.
|
||||
*
|
||||
* Copyright 2015 Oso Technologies
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
import groovy.json.JsonBuilder
|
||||
import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
|
||||
definition(
|
||||
name: "PlantLink Connector",
|
||||
namespace: "Osotech",
|
||||
author: "Oso Technologies",
|
||||
description: "This SmartApp connects to myplantlink.com and forwards the device data to it so it can calculate easy to read plant status for your specific plant's needs.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://dashboard.myplantlink.com/images/apple-touch-icon-76x76-precomposed.png",
|
||||
iconX2Url: "https://dashboard.myplantlink.com/images/apple-touch-icon-120x120-precomposed.png",
|
||||
iconX3Url: "https://dashboard.myplantlink.com/images/apple-touch-icon-152x152-precomposed.png"
|
||||
) {
|
||||
appSetting "client_id"
|
||||
appSetting "client_secret"
|
||||
appSetting "https_plantLinkServer"
|
||||
}
|
||||
|
||||
preferences {
|
||||
page(name: "auth", title: "Step 1 of 2", nextPage:"deviceList", content:"authPage")
|
||||
page(name: "deviceList", title: "Step 2 of 2", install:true, uninstall:false){
|
||||
section {
|
||||
input "plantlinksensors", "capability.sensor", title: "Select PlantLink sensors", multiple: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/swapToken") {
|
||||
action: [
|
||||
GET: "swapToken"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def authPage(){
|
||||
if(!atomicState.accessToken){
|
||||
createAccessToken()
|
||||
atomicState.accessToken = state.accessToken
|
||||
}
|
||||
|
||||
def redirectUrl = oauthInitUrl()
|
||||
def uninstallAllowed = false
|
||||
def oauthTokenProvided = false
|
||||
if(atomicState.authToken){
|
||||
uninstallAllowed = true
|
||||
oauthTokenProvided = true
|
||||
}
|
||||
|
||||
if (!oauthTokenProvided) {
|
||||
return dynamicPage(name: "auth", title: "Step 1 of 2", nextPage:null, uninstall:uninstallAllowed) {
|
||||
section(){
|
||||
href(name:"login",
|
||||
url:redirectUrl,
|
||||
style:"embedded",
|
||||
title:"PlantLink",
|
||||
image:"https://dashboard.myplantlink.com/images/PLlogo.png",
|
||||
description:"Tap to login to myplantlink.com")
|
||||
}
|
||||
}
|
||||
}else{
|
||||
return dynamicPage(name: "auth", title: "Step 1 of 2 - Completed", nextPage:"deviceList", uninstall:uninstallAllowed) {
|
||||
section(){
|
||||
paragraph "You are logged in to myplantlink.com, tap next to continue", image: iconUrl
|
||||
href(url:redirectUrl, title:"Or", description:"tap to switch accounts")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def uninstalled() {
|
||||
if (plantlinksensors){
|
||||
plantlinksensors.each{ sensor_device ->
|
||||
sensor_device.setInstallSmartApp("needSmartApp")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
atomicState.attached_sensors = [:]
|
||||
if (plantlinksensors){
|
||||
subscribe(plantlinksensors, "moisture_status", moistureHandler)
|
||||
subscribe(plantlinksensors, "battery_status", batteryHandler)
|
||||
plantlinksensors.each{ sensor_device ->
|
||||
sensor_device.setStatusIcon("Waiting on First Measurement")
|
||||
sensor_device.setInstallSmartApp("connectedToSmartApp")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def dock_sensor(device_serial, expected_plant_name) {
|
||||
def docking_body_json_builder = new JsonBuilder([version: '1c', smartthings_device_id: device_serial])
|
||||
def docking_params = [
|
||||
uri : appSettings.https_plantLinkServer,
|
||||
path : "/api/v1/smartthings/links",
|
||||
headers : ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
||||
contentType: "application/json",
|
||||
body: docking_body_json_builder.toString()
|
||||
]
|
||||
def plant_post_body_map = [
|
||||
plant_type_key: 999999,
|
||||
soil_type_key : 1000004
|
||||
]
|
||||
def plant_post_params = [
|
||||
uri : appSettings.https_plantLinkServer,
|
||||
path : "/api/v1/plants",
|
||||
headers : ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
||||
contentType: "application/json",
|
||||
]
|
||||
log.debug "Creating new plant on myplantlink.com - ${expected_plant_name}"
|
||||
httpPost(docking_params) { docking_response ->
|
||||
if (parse_api_response(docking_response, "Docking a link")) {
|
||||
if (docking_response.data.plants.size() == 0) {
|
||||
log.debug "creating plant for - ${expected_plant_name}"
|
||||
plant_post_body_map["name"] = expected_plant_name
|
||||
plant_post_body_map['links_key'] = [docking_response.data.key]
|
||||
def plant_post_body_json_builder = new JsonBuilder(plant_post_body_map)
|
||||
plant_post_params["body"] = plant_post_body_json_builder.toString()
|
||||
httpPost(plant_post_params) { plant_post_response ->
|
||||
if(parse_api_response(plant_post_response, 'creating plant')){
|
||||
def attached_map = atomicState.attached_sensors
|
||||
attached_map[device_serial] = plant_post_response.data
|
||||
atomicState.attached_sensors = attached_map
|
||||
}
|
||||
}
|
||||
} else {
|
||||
def plant = docking_response.data.plants[0]
|
||||
def attached_map = atomicState.attached_sensors
|
||||
attached_map[device_serial] = plant
|
||||
atomicState.attached_sensors = attached_map
|
||||
checkAndUpdatePlantIfNeeded(plant, expected_plant_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
def checkAndUpdatePlantIfNeeded(plant, expected_plant_name){
|
||||
def plant_put_params = [
|
||||
uri : appSettings.https_plantLinkServer,
|
||||
headers : ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
||||
contentType : "application/json"
|
||||
]
|
||||
if (plant.name != expected_plant_name) {
|
||||
log.debug "updating plant for - ${expected_plant_name}"
|
||||
plant_put_params["path"] = "/api/v1/plants/${plant.key}"
|
||||
def plant_put_body_map = [
|
||||
name: expected_plant_name
|
||||
]
|
||||
def plant_put_body_json_builder = new JsonBuilder(plant_put_body_map)
|
||||
plant_put_params["body"] = plant_put_body_json_builder.toString()
|
||||
httpPut(plant_put_params) { plant_put_response ->
|
||||
parse_api_response(plant_put_response, 'updating plant name')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def moistureHandler(event){
|
||||
def expected_plant_name = "SmartThings - ${event.displayName}"
|
||||
def device_serial = getDeviceSerialFromEvent(event)
|
||||
|
||||
if (!atomicState.attached_sensors.containsKey(device_serial)){
|
||||
dock_sensor(device_serial, expected_plant_name)
|
||||
}else{
|
||||
def measurement_post_params = [
|
||||
uri: appSettings.https_plantLinkServer,
|
||||
path: "/api/v1/smartthings/links/${device_serial}/measurements",
|
||||
headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
||||
contentType: "application/json",
|
||||
body: event.value
|
||||
]
|
||||
httpPost(measurement_post_params) { measurement_post_response ->
|
||||
if (parse_api_response(measurement_post_response, 'creating moisture measurement') &&
|
||||
measurement_post_response.data.size() >0){
|
||||
def measurement = measurement_post_response.data[0]
|
||||
def plant = measurement.plant
|
||||
log.debug plant
|
||||
checkAndUpdatePlantIfNeeded(plant, expected_plant_name)
|
||||
plantlinksensors.each{ sensor_device ->
|
||||
if (sensor_device.id == event.deviceId){
|
||||
sensor_device.setStatusIcon(plant.status)
|
||||
if (plant.last_measurements && plant.last_measurements[0].moisture){
|
||||
sensor_device.setPlantFuelLevel(plant.last_measurements[0].moisture * 100 as int)
|
||||
}
|
||||
if (plant.last_measurements && plant.last_measurements[0].battery){
|
||||
sensor_device.setBatteryLevel(plant.last_measurements[0].battery * 100 as int)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def batteryHandler(event){
|
||||
def expected_plant_name = "SmartThings - ${event.displayName}"
|
||||
def device_serial = getDeviceSerialFromEvent(event)
|
||||
|
||||
if (!atomicState.attached_sensors.containsKey(device_serial)){
|
||||
dock_sensor(device_serial, expected_plant_name)
|
||||
}else{
|
||||
def measurement_post_params = [
|
||||
uri: appSettings.https_plantLinkServer,
|
||||
path: "/api/v1/smartthings/links/${device_serial}/measurements",
|
||||
headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"],
|
||||
contentType: "application/json",
|
||||
body: event.value
|
||||
]
|
||||
httpPost(measurement_post_params) { measurement_post_response ->
|
||||
parse_api_response(measurement_post_response, 'creating battery measurement')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getDeviceSerialFromEvent(event){
|
||||
def pattern = /.*"zigbeedeviceid"\s*:\s*"(\w+)".*/
|
||||
def match_result = (event.value =~ pattern)
|
||||
return match_result[0][1]
|
||||
}
|
||||
|
||||
def oauthInitUrl(){
|
||||
atomicState.oauthInitState = UUID.randomUUID().toString()
|
||||
def oauthParams = [
|
||||
response_type: "code",
|
||||
client_id: appSettings.client_id,
|
||||
state: atomicState.oauthInitState,
|
||||
redirect_uri: buildRedirectUrl()
|
||||
]
|
||||
return appSettings.https_plantLinkServer + "/oauth/oauth2/authorize?" + toQueryString(oauthParams)
|
||||
}
|
||||
|
||||
def buildRedirectUrl(){
|
||||
return getServerUrl() + "/api/token/${atomicState.accessToken}/smartapps/installations/${app.id}/swapToken"
|
||||
}
|
||||
|
||||
def swapToken(){
|
||||
def code = params.code
|
||||
def oauthState = params.state
|
||||
def stcid = appSettings.client_id
|
||||
def postParams = [
|
||||
method: 'POST',
|
||||
uri: "https://oso-tech.appspot.com",
|
||||
path: "/api/v1/oauth-token",
|
||||
query: [grant_type:'authorization_code', code:params.code, client_id:stcid,
|
||||
client_secret:appSettings.client_secret, redirect_uri: buildRedirectUrl()],
|
||||
]
|
||||
|
||||
def jsonMap
|
||||
httpPost(postParams) { resp ->
|
||||
jsonMap = resp.data
|
||||
}
|
||||
|
||||
atomicState.refreshToken = jsonMap.refresh_token
|
||||
atomicState.authToken = jsonMap.access_token
|
||||
|
||||
def html = """
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
.container {
|
||||
padding:25px;
|
||||
}
|
||||
.flex1 {
|
||||
width:33%;
|
||||
float:left;
|
||||
text-align: center;
|
||||
}
|
||||
p {
|
||||
font-size: 2em;
|
||||
font-family: Verdana, Geneva, sans-serif;
|
||||
text-align: center;
|
||||
color: #777;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="flex1"><img src="https://dashboard.myplantlink.com/images/PLlogo.png" alt="PlantLink" height="75"/></div>
|
||||
<div class="flex1"><img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png" alt="connected to" height="25" style="padding-top:25px;" /></div>
|
||||
<div class="flex1"><img src="https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png" alt="SmartThings" height="75"/></div>
|
||||
<br clear="all">
|
||||
</div>
|
||||
<div class="container">
|
||||
<p>Your PlantLink Account is now connected to SmartThings!</p>
|
||||
<p style="color:green;">Click <strong>Done</strong> at the top right to finish setup.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
render contentType: 'text/html', data: html
|
||||
}
|
||||
|
||||
private refreshAuthToken() {
|
||||
def stcid = appSettings.client_id
|
||||
def refreshParams = [
|
||||
method: 'POST',
|
||||
uri: "https://hardware-dot-oso-tech.appspot.com",
|
||||
path: "/api/v1/oauth-token",
|
||||
query: [grant_type:'refresh_token', code:"${atomicState.refreshToken}", client_id:stcid,
|
||||
client_secret:appSettings.client_secret],
|
||||
]
|
||||
try{
|
||||
def jsonMap
|
||||
httpPost(refreshParams) { resp ->
|
||||
if(resp.status == 200){
|
||||
log.debug "OAuth Token refreshed"
|
||||
jsonMap = resp.data
|
||||
if (resp.data) {
|
||||
atomicState.refreshToken = resp?.data?.refresh_token
|
||||
atomicState.authToken = resp?.data?.access_token
|
||||
if (data?.action && data?.action != "") {
|
||||
log.debug data.action
|
||||
"{data.action}"()
|
||||
data.action = ""
|
||||
}
|
||||
}
|
||||
data.action = ""
|
||||
}else{
|
||||
log.debug "refresh failed ${resp.status} : ${resp.status.code}"
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e){
|
||||
log.debug "caught exception refreshing auth token: " + e
|
||||
}
|
||||
}
|
||||
|
||||
def parse_api_response(resp, message) {
|
||||
if (resp.status == 200) {
|
||||
return true
|
||||
} else {
|
||||
log.error "sent ${message} Json & got http status ${resp.status} - ${resp.status.code}"
|
||||
if (resp.status == 401) {
|
||||
refreshAuthToken()
|
||||
return false
|
||||
} else {
|
||||
debugEvent("Authentication error, invalid authentication method, lack of credentials, etc.", true)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getServerUrl() {
|
||||
return "https://graph.api.smartthings.com"
|
||||
}
|
||||
|
||||
def debugEvent(message, displayEvent) {
|
||||
def results = [
|
||||
name: "appdebug",
|
||||
descriptionText: message,
|
||||
displayed: displayEvent
|
||||
]
|
||||
log.debug "Generating AppDebug Event: ${results}"
|
||||
sendEvent (results)
|
||||
}
|
||||
|
||||
def toQueryString(Map m){
|
||||
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
||||
}
|
||||
@@ -14,8 +14,8 @@
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Smart Lock / Unlock",
|
||||
namespace: "",
|
||||
name: "Smart Auto Lock / Unlock",
|
||||
namespace: "smart-auto-lock-unlock",
|
||||
author: "Arnaud",
|
||||
description: "Automatically locks door X minutes after being closed and keeps door unlocked if door is open.",
|
||||
category: "Safety & Security",
|
||||
@@ -0,0 +1,607 @@
|
||||
/**
|
||||
* Bose SoundTouch (Connect)
|
||||
*
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Bose SoundTouch (Connect)",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings",
|
||||
description: "Control your Bose SoundTouch speakers",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
||||
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
|
||||
singleInstance: true
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name:"deviceDiscovery", title:"Device Setup", content:"deviceDiscovery", refreshTimeout:5)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the urn that we're looking for
|
||||
*
|
||||
* @return URN which we are looking for
|
||||
*
|
||||
* @todo This + getUSNQualifier should be one and should use regular expressions
|
||||
*/
|
||||
def getDeviceType() {
|
||||
return "urn:schemas-upnp-org:device:MediaRenderer:1" // Bose
|
||||
}
|
||||
|
||||
/**
|
||||
* If not null, returns an additional qualifier for ssdUSN
|
||||
* to avoid spamming the network
|
||||
*
|
||||
* @return Additional qualifier OR null if not needed
|
||||
*/
|
||||
def getUSNQualifier() {
|
||||
return "uuid:BO5EBO5E-F00D-F00D-FEED-"
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the new device to instantiate in the user's smartapps
|
||||
* This must be an app owned by the namespace (see #getNameSpace).
|
||||
*
|
||||
* @return name
|
||||
*/
|
||||
def getDeviceName() {
|
||||
return "Bose SoundTouch"
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the namespace this app and siblings use
|
||||
*
|
||||
* @return namespace
|
||||
*/
|
||||
def getNameSpace() {
|
||||
return "smartthings"
|
||||
}
|
||||
|
||||
/**
|
||||
* The deviceDiscovery page used by preferences. Will automatically
|
||||
* make calls to the underlying discovery mechanisms as well as update
|
||||
* whenever new devices are discovered AND verified.
|
||||
*
|
||||
* @return a dynamicPage() object
|
||||
*/
|
||||
def deviceDiscovery()
|
||||
{
|
||||
if(canInstallLabs())
|
||||
{
|
||||
def refreshInterval = 3 // Number of seconds between refresh
|
||||
int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int
|
||||
state.deviceRefreshCount = deviceRefreshCount + refreshInterval
|
||||
|
||||
def devices = getSelectableDevice()
|
||||
def numFound = devices.size() ?: 0
|
||||
|
||||
// Make sure we get location updates (contains LAN data such as SSDP results, etc)
|
||||
subscribeNetworkEvents()
|
||||
|
||||
//device discovery request every 15s
|
||||
if((deviceRefreshCount % 15) == 0) {
|
||||
discoverDevices()
|
||||
}
|
||||
|
||||
// Verify request every 3 seconds except on discoveries
|
||||
if(((deviceRefreshCount % 3) == 0) && ((deviceRefreshCount % 15) != 0)) {
|
||||
verifyDevices()
|
||||
}
|
||||
|
||||
log.trace "Discovered devices: ${devices}"
|
||||
|
||||
return dynamicPage(name:"deviceDiscovery", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
||||
section("Please wait while we discover your ${getDeviceName()}. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||
input "selecteddevice", "enum", required:false, title:"Select ${getDeviceName()} (${numFound} found)", multiple:true, options:devices
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
|
||||
|
||||
To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub"."""
|
||||
|
||||
return dynamicPage(name:"deviceDiscovery", title:"Upgrade needed!", nextPage:"", install:true, uninstall: true) {
|
||||
section("Upgrade") {
|
||||
paragraph "$upgradeNeeded"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by SmartThings Cloud when user has selected device(s) and
|
||||
* pressed "Install".
|
||||
*/
|
||||
def installed() {
|
||||
log.trace "Installed with settings: ${settings}"
|
||||
initialize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by SmartThings Cloud when app has been updated
|
||||
*/
|
||||
def updated() {
|
||||
log.trace "Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by SmartThings Cloud when user uninstalls the app
|
||||
*
|
||||
* We don't need to manually do anything here because any children
|
||||
* are automatically removed upon the removal of the parent.
|
||||
*
|
||||
* Only time to do anything here is when you need to notify
|
||||
* the remote end. And even then you're discouraged from removing
|
||||
* the children manually.
|
||||
*/
|
||||
def uninstalled() {
|
||||
}
|
||||
|
||||
/**
|
||||
* If user has selected devices, will start monitoring devices
|
||||
* for changes (new address, port, etc...)
|
||||
*/
|
||||
def initialize() {
|
||||
log.trace "initialize()"
|
||||
state.subscribe = false
|
||||
if (selecteddevice) {
|
||||
addDevice()
|
||||
refreshDevices()
|
||||
subscribeNetworkEvents(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the child devices based on the user's selection
|
||||
*
|
||||
* Uses selecteddevice defined in the deviceDiscovery() page
|
||||
*/
|
||||
def addDevice(){
|
||||
def devices = getVerifiedDevices()
|
||||
def devlist
|
||||
log.trace "Adding childs"
|
||||
|
||||
// If only one device is selected, we don't get a list (when using simulator)
|
||||
if (!(selecteddevice instanceof List)) {
|
||||
devlist = [selecteddevice]
|
||||
} else {
|
||||
devlist = selecteddevice
|
||||
}
|
||||
|
||||
log.trace "These are being installed: ${devlist}"
|
||||
|
||||
devlist.each { dni ->
|
||||
def d = getChildDevice(dni)
|
||||
if(!d) {
|
||||
def newDevice = devices.find { (it.value.mac) == dni }
|
||||
def deviceName = newDevice?.value.name
|
||||
if (!deviceName)
|
||||
deviceName = getDeviceName() + "[${newDevice?.value.name}]"
|
||||
d = addChildDevice(getNameSpace(), getDeviceName(), dni, newDevice?.value.hub, [label:"${deviceName}"])
|
||||
d.boseSetDeviceID(newDevice.value.deviceID)
|
||||
log.trace "Created ${d.displayName} with id $dni"
|
||||
} else {
|
||||
log.trace "${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a DeviceNetworkId to an address. Primarily used by children
|
||||
*
|
||||
* @param dni Device Network id
|
||||
* @return address or null
|
||||
*/
|
||||
def resolveDNI2Address(dni) {
|
||||
def device = getVerifiedDevices().find { (it.value.mac) == dni }
|
||||
if (device) {
|
||||
return convertHexToIP(device.value.networkAddress)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins a child to the "Play Everywhere" zone
|
||||
*
|
||||
* @param child The speaker joining the zone
|
||||
* @return A list of maps with POST data
|
||||
*/
|
||||
def boseZoneJoin(child) {
|
||||
log = child.log // So we can debug this function
|
||||
|
||||
def results = []
|
||||
def result = [:]
|
||||
|
||||
// Find the master (if any)
|
||||
def server = getChildDevices().find{ it.boseGetZone() == "server" }
|
||||
|
||||
if (server) {
|
||||
log.debug "boseJoinZone() We have a server already, so lets add the new speaker"
|
||||
child.boseSetZone("client")
|
||||
|
||||
result['endpoint'] = "/setZone"
|
||||
result['host'] = server.getDeviceIP() + ":8090"
|
||||
result['body'] = "<zone master=\"${server.boseGetDeviceID()}\" senderIPAddress=\"${server.getDeviceIP()}\">"
|
||||
getChildDevices().each{ it ->
|
||||
log.trace "child: " + child
|
||||
log.trace "zone : " + it.boseGetZone()
|
||||
if (it.boseGetZone() || it.boseGetDeviceID() == child.boseGetDeviceID())
|
||||
result['body'] = result['body'] + "<member ipaddress=\"${it.getDeviceIP()}\">${it.boseGetDeviceID()}</member>"
|
||||
}
|
||||
result['body'] = result['body'] + '</zone>'
|
||||
} else {
|
||||
log.debug "boseJoinZone() No server, add it!"
|
||||
result['endpoint'] = "/setZone"
|
||||
result['host'] = child.getDeviceIP() + ":8090"
|
||||
result['body'] = "<zone master=\"${child.boseGetDeviceID()}\" senderIPAddress=\"${child.getDeviceIP()}\">"
|
||||
result['body'] = result['body'] + "<member ipaddress=\"${child.getDeviceIP()}\">${child.boseGetDeviceID()}</member>"
|
||||
result['body'] = result['body'] + '</zone>'
|
||||
child.boseSetZone("server")
|
||||
}
|
||||
results << result
|
||||
return results
|
||||
}
|
||||
|
||||
def boseZoneReset() {
|
||||
getChildDevices().each{ it.boseSetZone(null) }
|
||||
}
|
||||
|
||||
def boseZoneHasMaster() {
|
||||
return getChildDevices().find{ it.boseGetZone() == "server" } != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a speaker from the play everywhere zone.
|
||||
*
|
||||
* @param child Which speaker is leaving
|
||||
* @return a list of maps with POST data
|
||||
*/
|
||||
def boseZoneLeave(child) {
|
||||
log = child.log // So we can debug this function
|
||||
|
||||
def results = []
|
||||
def result = [:]
|
||||
|
||||
// First, tag us as a non-member
|
||||
child.boseSetZone(null)
|
||||
|
||||
// Find the master (if any)
|
||||
def server = getChildDevices().find{ it.boseGetZone() == "server" }
|
||||
|
||||
if (server && server.boseGetDeviceID() != child.boseGetDeviceID()) {
|
||||
log.debug "boseLeaveZone() We have a server, so tell him we're leaving"
|
||||
result['endpoint'] = "/removeZoneSlave"
|
||||
result['host'] = server.getDeviceIP() + ":8090"
|
||||
result['body'] = "<zone master=\"${server.boseGetDeviceID()}\" senderIPAddress=\"${server.getDeviceIP()}\">"
|
||||
result['body'] = result['body'] + "<member ipaddress=\"${child.getDeviceIP()}\">${child.boseGetDeviceID()}</member>"
|
||||
result['body'] = result['body'] + '</zone>'
|
||||
results << result
|
||||
} else {
|
||||
log.debug "boseLeaveZone() No server, then...uhm, we probably were it!"
|
||||
// Dismantle the entire thing, first send this to master
|
||||
result['endpoint'] = "/removeZoneSlave"
|
||||
result['host'] = child.getDeviceIP() + ":8090"
|
||||
result['body'] = "<zone master=\"${child.boseGetDeviceID()}\" senderIPAddress=\"${child.getDeviceIP()}\">"
|
||||
getChildDevices().each{ dev ->
|
||||
if (dev.boseGetZone() || dev.boseGetDeviceID() == child.boseGetDeviceID())
|
||||
result['body'] = result['body'] + "<member ipaddress=\"${dev.getDeviceIP()}\">${dev.boseGetDeviceID()}</member>"
|
||||
}
|
||||
result['body'] = result['body'] + '</zone>'
|
||||
results << result
|
||||
|
||||
// Also issue this to each individual client
|
||||
getChildDevices().each{ dev ->
|
||||
if (dev.boseGetZone() && dev.boseGetDeviceID() != child.boseGetDeviceID()) {
|
||||
log.trace "Additional device: " + dev
|
||||
result['host'] = dev.getDeviceIP() + ":8090"
|
||||
results << result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Define our XML parsers
|
||||
*
|
||||
* @return mapping of root-node <-> parser function
|
||||
*/
|
||||
def getParsers() {
|
||||
[
|
||||
"root" : "parseDESC",
|
||||
"info" : "parseINFO"
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when location has changed, contains information from
|
||||
* network transactions. See deviceDiscovery() for where it is
|
||||
* registered.
|
||||
*
|
||||
* @param evt Holds event information
|
||||
*/
|
||||
def onLocation(evt) {
|
||||
// Convert the event into something we can use
|
||||
def lanEvent = parseLanMessage(evt.description, true)
|
||||
lanEvent << ["hub":evt?.hubId]
|
||||
|
||||
// Determine what we need to do...
|
||||
if (lanEvent?.ssdpTerm?.contains(getDeviceType()) &&
|
||||
(getUSNQualifier() == null ||
|
||||
lanEvent?.ssdpUSN?.contains(getUSNQualifier())
|
||||
)
|
||||
)
|
||||
{
|
||||
parseSSDP(lanEvent)
|
||||
}
|
||||
else if (
|
||||
lanEvent.headers && lanEvent.body &&
|
||||
lanEvent.headers."content-type".contains("xml")
|
||||
)
|
||||
{
|
||||
def parsers = getParsers()
|
||||
def xmlData = new XmlSlurper().parseText(lanEvent.body)
|
||||
|
||||
// Let each parser take a stab at it
|
||||
parsers.each { node,func ->
|
||||
if (xmlData.name() == node)
|
||||
"$func"(xmlData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles SSDP description file.
|
||||
*
|
||||
* @param xmlData
|
||||
*/
|
||||
private def parseDESC(xmlData) {
|
||||
log.info "parseDESC()"
|
||||
|
||||
def devicetype = getDeviceType().toLowerCase()
|
||||
def devicetxml = body.device.deviceType.text().toLowerCase()
|
||||
|
||||
// Make sure it's the type we want
|
||||
if (devicetxml == devicetype) {
|
||||
def devices = getDevices()
|
||||
def device = devices.find {it?.key?.contains(xmlData?.device?.UDN?.text())}
|
||||
if (device && !device.value?.verified) {
|
||||
// Unlike regular DESC, we cannot trust this just yet, parseINFO() decides all
|
||||
device.value << [name:xmlData?.device?.friendlyName?.text(),model:xmlData?.device?.modelName?.text(), serialNumber:xmlData?.device?.serialNum?.text()]
|
||||
} else {
|
||||
log.error "parseDESC(): The xml file returned a device that didn't exist"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle BOSE <info></info> result. This is an alternative to
|
||||
* using the SSDP description standard. Some of the speakers do
|
||||
* not support SSDP description, so we need this as well.
|
||||
*
|
||||
* @param xmlData
|
||||
*/
|
||||
private def parseINFO(xmlData) {
|
||||
log.info "parseINFO()"
|
||||
def devicetype = getDeviceType().toLowerCase()
|
||||
|
||||
def deviceID = xmlData.attributes()['deviceID']
|
||||
def device = getDevices().find {it?.key?.contains(deviceID)}
|
||||
if (device && !device.value?.verified) {
|
||||
device.value << [name:xmlData?.name?.text(),model:xmlData?.type?.text(), serialNumber:xmlData?.serialNumber?.text(), "deviceID":deviceID, verified: true]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles SSDP discovery messages and adds them to the list
|
||||
* of discovered devices. If it already exists, it will update
|
||||
* the port and location (in case it was moved).
|
||||
*
|
||||
* @param lanEvent
|
||||
*/
|
||||
def parseSSDP(lanEvent) {
|
||||
//SSDP DISCOVERY EVENTS
|
||||
def USN = lanEvent.ssdpUSN.toString()
|
||||
def devices = getDevices()
|
||||
|
||||
if (!(devices."${USN}")) {
|
||||
//device does not exist
|
||||
log.trace "parseSDDP() Adding Device \"${USN}\" to known list"
|
||||
devices << ["${USN}":lanEvent]
|
||||
} else {
|
||||
// update the values
|
||||
def d = devices."${USN}"
|
||||
if (d.networkAddress != lanEvent.networkAddress || d.deviceAddress != lanEvent.deviceAddress) {
|
||||
log.trace "parseSSDP() Updating device location (ip & port)"
|
||||
d.networkAddress = lanEvent.networkAddress
|
||||
d.deviceAddress = lanEvent.deviceAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a Map object which can be used with a preference page
|
||||
* to represent a list of devices detected and verified.
|
||||
*
|
||||
* @return Map with zero or more devices
|
||||
*/
|
||||
Map getSelectableDevice() {
|
||||
def devices = getVerifiedDevices()
|
||||
def map = [:]
|
||||
devices.each {
|
||||
def value = "${it.value.name}"
|
||||
def key = it.value.mac
|
||||
map["${key}"] = value
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the refresh loop, making sure to keep us up-to-date with changes
|
||||
*
|
||||
*/
|
||||
private refreshDevices() {
|
||||
discoverDevices()
|
||||
verifyDevices()
|
||||
runIn(300, "refreshDevices")
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a subscription for network events
|
||||
*
|
||||
* @param force If true, will unsubscribe and subscribe if necessary (Optional, default false)
|
||||
*/
|
||||
private subscribeNetworkEvents(force=false) {
|
||||
if (force) {
|
||||
unsubscribe()
|
||||
state.subscribe = false
|
||||
}
|
||||
|
||||
if(!state.subscribe) {
|
||||
subscribe(location, null, onLocation, [filterEvents:false])
|
||||
state.subscribe = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Issues a SSDP M-SEARCH over the LAN for a specific type (see getDeviceType())
|
||||
*/
|
||||
private discoverDevices() {
|
||||
log.trace "discoverDevice() Issuing SSDP request"
|
||||
sendHubCommand(new physicalgraph.device.HubAction("lan discovery ${getDeviceType()}", physicalgraph.device.Protocol.LAN))
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks through the list of unverified devices and issues a verification
|
||||
* request for each of them (basically calling verifyDevice() per unverified)
|
||||
*/
|
||||
private verifyDevices() {
|
||||
def devices = getDevices().findAll { it?.value?.verified != true }
|
||||
|
||||
devices.each {
|
||||
verifyDevice(
|
||||
it?.value?.mac,
|
||||
convertHexToIP(it?.value?.networkAddress),
|
||||
convertHexToInt(it?.value?.deviceAddress),
|
||||
it?.value?.ssdpPath
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the device, in this case, we need to obtain the info block which
|
||||
* holds information such as the actual mac to use in certain scenarios.
|
||||
*
|
||||
* Without this mac (henceforth referred to as deviceID), we can't do multi-speaker
|
||||
* functions.
|
||||
*
|
||||
* @param deviceNetworkId The DNI of the device
|
||||
* @param ip The address of the device on the network (not the same as DNI)
|
||||
* @param port The port to use (0 will be treated as invalid and will use 80)
|
||||
* @param devicessdpPath The URL path (for example, /desc)
|
||||
*
|
||||
* @note Result is captured in locationHandler()
|
||||
*/
|
||||
private verifyDevice(String deviceNetworkId, String ip, int port, String devicessdpPath) {
|
||||
if(ip) {
|
||||
def address = ip + ":8090"
|
||||
sendHubCommand(new physicalgraph.device.HubAction([
|
||||
method: "GET",
|
||||
path: "/info",
|
||||
headers: [
|
||||
HOST: address,
|
||||
]]))
|
||||
} else {
|
||||
log.warn("verifyDevice() IP address was empty")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of devices which have been verified
|
||||
*
|
||||
* @return array of verified devices
|
||||
*/
|
||||
def getVerifiedDevices() {
|
||||
getDevices().findAll{ it?.value?.verified == true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all discovered devices or an empty array if none
|
||||
*
|
||||
* @return array of devices
|
||||
*/
|
||||
def getDevices() {
|
||||
state.devices = state.devices ?: [:]
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a hexadecimal string to an integer
|
||||
*
|
||||
* @param hex The string with a hexadecimal value
|
||||
* @return An integer
|
||||
*/
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an IP address represented as 0xAABBCCDD to AAA.BBB.CCC.DDD
|
||||
*
|
||||
* @param hex Address represented in hex
|
||||
* @return String containing normal IPv4 dot notation
|
||||
*/
|
||||
private String convertHexToIP(hex) {
|
||||
if (hex)
|
||||
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
|
||||
else
|
||||
hex
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if this setup can support SmarthThing Labs items
|
||||
*
|
||||
* @return true if it supports it.
|
||||
*/
|
||||
private Boolean canInstallLabs()
|
||||
{
|
||||
return hasAllHubsOver("000.011.00603")
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if the firmwares on all hubs owned by user match or exceed the
|
||||
* provided version number.
|
||||
*
|
||||
* @param desiredFirmware The version that must match or exceed
|
||||
* @return true if hub has same or newer
|
||||
*/
|
||||
private Boolean hasAllHubsOver(String desiredFirmware)
|
||||
{
|
||||
return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of firmware version for every hub the user has
|
||||
*
|
||||
* @return List of firmwares
|
||||
*/
|
||||
private List getRealHubFirmwareVersions()
|
||||
{
|
||||
return location.hubs*.firmwareVersionString.findAll { it }
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
/**
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* Bose® SoundTouch® Control
|
||||
*
|
||||
* Author: SmartThings & Joe Geiger
|
||||
*
|
||||
* Date: 2015-30-09
|
||||
*/
|
||||
definition(
|
||||
name: "Bose® SoundTouch® Control",
|
||||
namespace: "smartthings",
|
||||
author: "SmartThings & Joe Geiger",
|
||||
description: "Control your Bose® SoundTouch® when certain actions take place in your home.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://d3azp77rte0gip.cloudfront.net/smartapps/fcf1d93a-ba0b-4324-b96f-e5b5487dfaf5/images/BoseST_icon.png",
|
||||
iconX2Url: "https://d3azp77rte0gip.cloudfront.net/smartapps/fcf1d93a-ba0b-4324-b96f-e5b5487dfaf5/images/BoseST_icon@2x.png",
|
||||
iconX3Url: "https://d3azp77rte0gip.cloudfront.net/smartapps/fcf1d93a-ba0b-4324-b96f-e5b5487dfaf5/images/BoseST_icon@2x-1.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name: "mainPage", title: "Control your Bose® SoundTouch® when something happens", install: true, uninstall: true)
|
||||
page(name: "timeIntervalInput", title: "Only during a certain time") {
|
||||
section {
|
||||
input "starting", "time", title: "Starting", required: false
|
||||
input "ending", "time", title: "Ending", required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def mainPage() {
|
||||
dynamicPage(name: "mainPage") {
|
||||
def anythingSet = anythingSet()
|
||||
if (anythingSet) {
|
||||
section("When..."){
|
||||
ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
|
||||
ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
|
||||
ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
|
||||
ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
|
||||
ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
|
||||
ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
|
||||
ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
|
||||
ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
|
||||
ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
|
||||
ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
|
||||
ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
|
||||
ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
|
||||
ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false
|
||||
}
|
||||
}
|
||||
section(anythingSet ? "Select additional triggers" : "When...", hideable: anythingSet, hidden: true){
|
||||
ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
|
||||
ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
|
||||
ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
|
||||
ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
|
||||
ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
|
||||
ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
|
||||
ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
|
||||
ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
|
||||
ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
|
||||
ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
|
||||
ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
|
||||
ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
|
||||
ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
|
||||
}
|
||||
section("Perform this action"){
|
||||
input "actionType", "enum", title: "Action?", required: true, defaultValue: "play", options: [
|
||||
"Turn On & Play",
|
||||
"Turn Off",
|
||||
"Toggle Play/Pause",
|
||||
"Skip to Next Track",
|
||||
"Skip to Beginning/Previous Track",
|
||||
"Play Preset 1",
|
||||
"Play Preset 2",
|
||||
"Play Preset 3",
|
||||
"Play Preset 4",
|
||||
"Play Preset 5",
|
||||
"Play Preset 6"
|
||||
]
|
||||
}
|
||||
section {
|
||||
input "bose", "capability.musicPlayer", title: "Bose® SoundTouch® music player", required: true
|
||||
}
|
||||
section("More options", hideable: true, hidden: true) {
|
||||
input "volume", "number", title: "Set the volume volume", description: "0-100%", required: false
|
||||
input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false
|
||||
href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
|
||||
input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
|
||||
options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||||
if (settings.modes) {
|
||||
input "modes", "mode", title: "Only when mode is", multiple: true, required: false
|
||||
}
|
||||
input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
|
||||
}
|
||||
section([mobileOnly:true]) {
|
||||
label title: "Assign a name", required: false
|
||||
mode title: "Set for specific mode(s)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private anythingSet() {
|
||||
for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","triggerModes","timeOfDay"]) {
|
||||
if (settings[name]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private ifUnset(Map options, String name, String capability) {
|
||||
if (!settings[name]) {
|
||||
input(options, name, capability)
|
||||
}
|
||||
}
|
||||
|
||||
private ifSet(Map options, String name, String capability) {
|
||||
if (settings[name]) {
|
||||
input(options, name, capability)
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
subscribeToEvents()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
unsubscribe()
|
||||
unschedule()
|
||||
subscribeToEvents()
|
||||
}
|
||||
|
||||
def subscribeToEvents() {
|
||||
log.trace "subscribeToEvents()"
|
||||
subscribe(app, appTouchHandler)
|
||||
subscribe(contact, "contact.open", eventHandler)
|
||||
subscribe(contactClosed, "contact.closed", eventHandler)
|
||||
subscribe(acceleration, "acceleration.active", eventHandler)
|
||||
subscribe(motion, "motion.active", eventHandler)
|
||||
subscribe(mySwitch, "switch.on", eventHandler)
|
||||
subscribe(mySwitchOff, "switch.off", eventHandler)
|
||||
subscribe(arrivalPresence, "presence.present", eventHandler)
|
||||
subscribe(departurePresence, "presence.not present", eventHandler)
|
||||
subscribe(smoke, "smoke.detected", eventHandler)
|
||||
subscribe(smoke, "smoke.tested", eventHandler)
|
||||
subscribe(smoke, "carbonMonoxide.detected", eventHandler)
|
||||
subscribe(water, "water.wet", eventHandler)
|
||||
subscribe(button1, "button.pushed", eventHandler)
|
||||
|
||||
if (triggerModes) {
|
||||
subscribe(location, modeChangeHandler)
|
||||
}
|
||||
|
||||
if (timeOfDay) {
|
||||
schedule(timeOfDay, scheduledTimeHandler)
|
||||
}
|
||||
}
|
||||
|
||||
def eventHandler(evt) {
|
||||
if (allOk) {
|
||||
def lastTime = state[frequencyKey(evt)]
|
||||
if (oncePerDayOk(lastTime)) {
|
||||
if (frequency) {
|
||||
if (lastTime == null || now() - lastTime >= frequency * 60000) {
|
||||
takeAction(evt)
|
||||
}
|
||||
else {
|
||||
log.debug "Not taking action because $frequency minutes have not elapsed since last action"
|
||||
}
|
||||
}
|
||||
else {
|
||||
takeAction(evt)
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.debug "Not taking action because it was already taken today"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def modeChangeHandler(evt) {
|
||||
log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
|
||||
if (evt.value in triggerModes) {
|
||||
eventHandler(evt)
|
||||
}
|
||||
}
|
||||
|
||||
def scheduledTimeHandler() {
|
||||
eventHandler(null)
|
||||
}
|
||||
|
||||
def appTouchHandler(evt) {
|
||||
takeAction(evt)
|
||||
}
|
||||
|
||||
private takeAction(evt) {
|
||||
log.debug "takeAction($actionType)"
|
||||
def options = [:]
|
||||
if (volume) {
|
||||
bose.setLevel(volume as Integer)
|
||||
options.delay = 1000
|
||||
}
|
||||
|
||||
switch (actionType) {
|
||||
case "Turn On & Play":
|
||||
options ? bose.on(options) : bose.on()
|
||||
break
|
||||
case "Turn Off":
|
||||
options ? bose.off(options) : bose.off()
|
||||
break
|
||||
case "Toggle Play/Pause":
|
||||
def currentStatus = bose.currentValue("playpause")
|
||||
if (currentStatus == "play") {
|
||||
options ? bose.pause(options) : bose.pause()
|
||||
}
|
||||
else if (currentStatus == "pause") {
|
||||
options ? bose.play(options) : bose.play()
|
||||
}
|
||||
break
|
||||
case "Skip to Next Track":
|
||||
options ? bose.nextTrack(options) : bose.nextTrack()
|
||||
break
|
||||
case "Skip to Beginning/Previous Track":
|
||||
options ? bose.previousTrack(options) : bose.previousTrack()
|
||||
break
|
||||
case "Play Preset 1":
|
||||
options ? bose.preset1(options) : bose.preset1()
|
||||
break
|
||||
case "Play Preset 2":
|
||||
options ? bose.preset2(options) : bose.preset2()
|
||||
break
|
||||
case "Play Preset 3":
|
||||
options ? bose.preset3(options) : bose.preset3()
|
||||
break
|
||||
case "Play Preset 4":
|
||||
options ? bose.preset4(options) : bose.preset4()
|
||||
break
|
||||
case "Play Preset 5":
|
||||
options ? bose.preset5(options) : bose.preset5()
|
||||
break
|
||||
case "Play Preset 6":
|
||||
options ? bose.preset6(options) : bose.preset6()
|
||||
break
|
||||
default:
|
||||
log.error "Action type '$actionType' not defined"
|
||||
}
|
||||
|
||||
if (frequency) {
|
||||
state.lastActionTimeStamp = now()
|
||||
}
|
||||
}
|
||||
|
||||
private frequencyKey(evt) {
|
||||
//evt.deviceId ?: evt.value
|
||||
"lastActionTimeStamp"
|
||||
}
|
||||
|
||||
private dayString(Date date) {
|
||||
def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
|
||||
if (location.timeZone) {
|
||||
df.setTimeZone(location.timeZone)
|
||||
}
|
||||
else {
|
||||
df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
|
||||
}
|
||||
df.format(date)
|
||||
}
|
||||
|
||||
private oncePerDayOk(Long lastTime) {
|
||||
def result = true
|
||||
if (oncePerDay) {
|
||||
result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
|
||||
log.trace "oncePerDayOk = $result"
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// TODO - centralize somehow
|
||||
private getAllOk() {
|
||||
modeOk && daysOk && timeOk
|
||||
}
|
||||
|
||||
private getModeOk() {
|
||||
def result = !modes || modes.contains(location.mode)
|
||||
log.trace "modeOk = $result"
|
||||
result
|
||||
}
|
||||
|
||||
private getDaysOk() {
|
||||
def result = true
|
||||
if (days) {
|
||||
def df = new java.text.SimpleDateFormat("EEEE")
|
||||
if (location.timeZone) {
|
||||
df.setTimeZone(location.timeZone)
|
||||
}
|
||||
else {
|
||||
df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
|
||||
}
|
||||
def day = df.format(new Date())
|
||||
result = days.contains(day)
|
||||
}
|
||||
log.trace "daysOk = $result"
|
||||
result
|
||||
}
|
||||
|
||||
private getTimeOk() {
|
||||
def result = true
|
||||
if (starting && ending) {
|
||||
def currTime = now()
|
||||
def start = timeToday(starting, location?.timeZone).time
|
||||
def stop = timeToday(ending, location?.timeZone).time
|
||||
result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
|
||||
}
|
||||
log.trace "timeOk = $result"
|
||||
result
|
||||
}
|
||||
|
||||
private hhmm(time, fmt = "h:mm a")
|
||||
{
|
||||
def t = timeToday(time, location.timeZone)
|
||||
def f = new java.text.SimpleDateFormat(fmt)
|
||||
f.setTimeZone(location.timeZone ?: timeZone(time))
|
||||
f.format(t)
|
||||
}
|
||||
|
||||
private timeIntervalLabel()
|
||||
{
|
||||
(starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
|
||||
}
|
||||
// TODO - End Centralize
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.8 KiB |
@@ -246,6 +246,9 @@ def toggle(devices) {
|
||||
else if (devices*.currentValue('lock').contains('locked')) {
|
||||
devices.unlock()
|
||||
}
|
||||
else if (devices*.currentValue('lock').contains('unlocked')) {
|
||||
devices.lock()
|
||||
}
|
||||
else if (devices*.currentValue('alarm').contains('off')) {
|
||||
devices.siren()
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Every Element
|
||||
*
|
||||
* Copyright 2014 SmartThings
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
@@ -14,349 +14,555 @@
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "Every Element",
|
||||
namespace: "smartthings/examples",
|
||||
author: "SmartThings",
|
||||
description: "Every element demonstration app",
|
||||
category: "SmartThings Internal",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png"
|
||||
name: "Every Element",
|
||||
namespace: "smartthings/examples",
|
||||
author: "SmartThings",
|
||||
description: "Every element demonstration app",
|
||||
category: "SmartThings Internal",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png"
|
||||
)
|
||||
|
||||
preferences {
|
||||
page(name: "firstPage")
|
||||
page(name: "inputPage")
|
||||
page(name: "appPage")
|
||||
page(name: "labelPage")
|
||||
page(name: "modePage")
|
||||
page(name: "paragraphPage")
|
||||
page(name: "iconPage")
|
||||
page(name: "hrefPage")
|
||||
page(name: "buttonsPage")
|
||||
page(name: "imagePage")
|
||||
page(name: "videoPage")
|
||||
page(name: "deadEnd", title: "Nothing to see here, move along.", content: "foo")
|
||||
page(name: "flattenedPage")
|
||||
// landing page
|
||||
page(name: "firstPage")
|
||||
|
||||
// PageKit
|
||||
page(name: "buttonsPage")
|
||||
page(name: "imagePage")
|
||||
page(name: "inputPage")
|
||||
page(name: "inputBooleanPage")
|
||||
page(name: "inputIconPage")
|
||||
page(name: "inputImagePage")
|
||||
page(name: "inputDevicePage")
|
||||
page(name: "inputCapabilityPage")
|
||||
page(name: "inputRoomPage")
|
||||
page(name: "inputModePage")
|
||||
page(name: "inputSelectionPage")
|
||||
page(name: "inputHubPage")
|
||||
page(name: "inputContactBookPage")
|
||||
page(name: "inputTextPage")
|
||||
page(name: "inputTimePage")
|
||||
page(name: "appPage")
|
||||
page(name: "hrefPage")
|
||||
page(name: "paragraphPage")
|
||||
page(name: "videoPage")
|
||||
page(name: "labelPage")
|
||||
page(name: "modePage")
|
||||
|
||||
// Every element helper pages
|
||||
page(name: "deadEnd", title: "Nothing to see here, move along.", content: "foo")
|
||||
page(name: "flattenedPage")
|
||||
}
|
||||
|
||||
def firstPage() {
|
||||
dynamicPage(name: "firstPage", title: "Where to first?", install: true, uninstall: true) {
|
||||
section() {
|
||||
href(page: "inputPage", title: "Element: 'input'")
|
||||
href(page: "appPage", title: "Element: 'app'")
|
||||
href(page: "labelPage", title: "Element: 'label'")
|
||||
href(page: "modePage", title: "Element: 'mode'")
|
||||
href(page: "paragraphPage", title: "Element: 'paragraph'")
|
||||
href(page: "iconPage", title: "Element: 'icon'")
|
||||
href(page: "hrefPage", title: "Element: 'href'")
|
||||
href(page: "buttonsPage", title: "Element: 'buttons'")
|
||||
href(page: "imagePage", title: "Element: 'image'")
|
||||
href(page: "videoPage", title: "Element: 'video'")
|
||||
}
|
||||
section() {
|
||||
href(page: "flattenedPage", title: "All of the above elements on a single page")
|
||||
}
|
||||
}
|
||||
dynamicPage(name: "firstPage", title: "Where to first?", install: true, uninstall: true) {
|
||||
section {
|
||||
href(page: "appPage", title: "Element: 'app'")
|
||||
href(page: "buttonsPage", title: "Element: 'buttons'")
|
||||
href(page: "hrefPage", title: "Element: 'href'")
|
||||
href(page: "imagePage", title: "Element: 'image'")
|
||||
href(page: "inputPage", title: "Element: 'input'")
|
||||
href(page: "labelPage", title: "Element: 'label'")
|
||||
href(page: "modePage", title: "Element: 'mode'")
|
||||
href(page: "paragraphPage", title: "Element: 'paragraph'")
|
||||
href(page: "videoPage", title: "Element: 'video'")
|
||||
}
|
||||
section {
|
||||
href(page: "flattenedPage", title: "All of the above elements on a single page")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def inputPage() {
|
||||
dynamicPage(name: "inputPage", title: "Every 'input' type") {
|
||||
section("enum") {
|
||||
input(type: "enum", name: "enumRefresh", title: "submitOnChange:true", required: false, multiple: true, options: ["one", "two", "three"], submitOnChange: true)
|
||||
if (enumRefresh) {
|
||||
paragraph "${enumRefresh}"
|
||||
}
|
||||
input(type: "enum", name: "enumSegmented", title: "style:segmented", required: false, multiple: true, options: ["one", "two", "three"], style: "segmented")
|
||||
input(type: "enum", name: "enum", title: "required:false, multiple:false", required: false, multiple: false, options: ["one", "two", "three"])
|
||||
input(type: "enum", name: "enumRequired", title: "required:true", required: true, multiple: false, options: ["one", "two", "three"])
|
||||
input(type: "enum", name: "enumMultiple", title: "multiple:true", required: false, multiple: true, options: ["one", "two", "three"])
|
||||
input(type: "enum", name: "enumWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, options: ["one", "two", "three"], image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
input(type: "enum", name: "enumWithGroupedOptions", title: "groupedOptions", description: "This enum has grouped options", required: false, multiple: true, groupedOptions: [
|
||||
[
|
||||
title : "the group title that is displayed",
|
||||
order : 0, // the order of the group; 0-based
|
||||
image : "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", // not yet supported
|
||||
values: [
|
||||
[
|
||||
key : "the value that will be placed in SmartApp settings.", // such as a device id
|
||||
value: "the title of the selectable option that is displayed", // such as a device name
|
||||
order: 0 // the order of the option
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
title : "the second group title that is displayed",
|
||||
order : 1, // the order of the group; 0-based
|
||||
image : null, // not yet supported
|
||||
values: [
|
||||
[
|
||||
key : "some_device_id",
|
||||
value: "some_device_name",
|
||||
order: 1 // the order of the option. This option will appear second in the list even though it is the first option defined in this map
|
||||
],
|
||||
[
|
||||
key : "some_other_device_id",
|
||||
value: "some_other_device_name",
|
||||
order: 0 // the order of the option. This option will appear first in the list even though it is not the first option defined in this map
|
||||
]
|
||||
]
|
||||
]
|
||||
])
|
||||
}
|
||||
section("text") {
|
||||
input(type: "text", name: "text", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
input(type: "text", name: "textRequired", title: "required:true", required: true, multiple: false)
|
||||
input(type: "text", name: "textWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
section("number") {
|
||||
input(type: "number", name: "number", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
input(type: "number", name: "numberRequired", title: "required:true", required: true, multiple: false)
|
||||
input(type: "number", name: "numberWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
section("boolean") {
|
||||
input(type: "boolean", name: "boolean", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
input(type: "boolean", name: "booleanWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
section("password") {
|
||||
input(type: "password", name: "password", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
input(type: "password", name: "passwordRequired", title: "required:true", required: true, multiple: false)
|
||||
input(type: "password", name: "passwordWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
section("phone") {
|
||||
input(type: "phone", name: "phone", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
input(type: "phone", name: "phoneRequired", title: "required:true", required: true, multiple: false)
|
||||
input(type: "phone", name: "phoneWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
section("email") {
|
||||
input(type: "email", name: "email", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
input(type: "email", name: "emailRequired", title: "required:true", required: true, multiple: false)
|
||||
input(type: "email", name: "emailWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
section("decimal") {
|
||||
input(type: "decimal", name: "decimal", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
input(type: "decimal", name: "decimalRequired", title: "required:true", required: true, multiple: false)
|
||||
input(type: "decimal", name: "decimalWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
section("mode") {
|
||||
input(type: "mode", name: "mode", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
input(type: "mode", name: "modeRequired", title: "required:true", required: true, multiple: false)
|
||||
input(type: "mode", name: "modeMultiple", title: "multiple:true", required: false, multiple: true)
|
||||
input(type: "mode", name: "iconWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
section("icon") {
|
||||
input(type: "icon", name: "icon", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
input(type: "icon", name: "iconRequired", title: "required:true", required: true, multiple: false)
|
||||
input(type: "icon", name: "iconWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
section("capability") {
|
||||
input(type: "capability.switch", name: "capability", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
input(type: "capability.switch", name: "capabilityRequired", title: "required:true", required: true, multiple: false)
|
||||
input(type: "capability.switch", name: "capabilityMultiple", title: "multiple:true", required: false, multiple: true)
|
||||
input(type: "capability.switch", name: "capabilityWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
section("hub") {
|
||||
input(type: "hub", name: "hub", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
input(type: "hub", name: "hubRequired", title: "required:true", required: true, multiple: false)
|
||||
input(type: "hub", name: "hubMultiple", title: "multiple:true", required: false, multiple: true)
|
||||
input(type: "hub", name: "hubWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
section("device") {
|
||||
input(type: "device.switch", name: "device", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
input(type: "device.switch", name: "deviceRequired", title: "required:true", required: true, multiple: false)
|
||||
input(type: "device.switch", name: "deviceMultiple", title: "multiple:true", required: false, multiple: true)
|
||||
input(type: "device.switch", name: "deviceWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
section("time") {
|
||||
input(type: "time", name: "time", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
input(type: "time", name: "timeRequired", title: "required:true", required: true, multiple: false)
|
||||
input(type: "time", name: "timeWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
section("contact-book") {
|
||||
input("recipients", "contact", title: "Notify", description: "Send notifications to") {
|
||||
input(type: "phone", name: "phone", title: "Send text message to", required: false, multiple: false)
|
||||
input(type: "boolean", name: "boolean", title: "Send push notification", required: false, multiple: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
dynamicPage(name: "inputPage", title: "Links to every 'input' element") {
|
||||
section {
|
||||
href(page: "inputBooleanPage", title: "to boolean page")
|
||||
href(page: "inputIconPage", title: "to icon page")
|
||||
href(page: "inputImagePage", title: "to image page")
|
||||
href(page: "inputSelectionPage", title: "to selection page")
|
||||
href(page: "inputTextPage", title: "to text page")
|
||||
href(page: "inputTimePage", title: "to time page")
|
||||
}
|
||||
section("subsets of selection input") {
|
||||
href(page: "inputDevicePage", title: "to device selection page")
|
||||
href(page: "inputCapabilityPage", title: "to capability selection page")
|
||||
href(page: "inputRoomPage", title: "to room selection page")
|
||||
href(page: "inputModePage", title: "to mode selection page")
|
||||
href(page: "inputHubPage", title: "to hub selection page")
|
||||
href(page: "inputContactBookPage", title: "to contact-book selection page")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def inputBooleanPage() {
|
||||
dynamicPage(name: "inputBooleanPage") {
|
||||
section {
|
||||
paragraph "The `required` and `multiple` attributes have no effect because the value will always be either `true` or `false`"
|
||||
}
|
||||
section {
|
||||
input(type: "boolean", name: "booleanWithoutDescription", title: "without description", description: null)
|
||||
input(type: "boolean", name: "booleanWithDescription", title: "with description", description: "This has a description")
|
||||
}
|
||||
section("defaultValue: 'true'") {
|
||||
input(type: "boolean", name: "booleanWithDefaultValue", title: "", description: "", defaultValue: "true")
|
||||
}
|
||||
section("with image") {
|
||||
input(type: "boolean", name: "booleanWithoutDescriptionWithImage", title: "without description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", description: null)
|
||||
input(type: "boolean", name: "booleanWithDescriptionWithImage", title: "with description", description: "This has a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
}
|
||||
}
|
||||
def inputIconPage() {
|
||||
dynamicPage(name: "inputIconPage") {
|
||||
section {
|
||||
paragraph "`description` is not displayed for icon elements"
|
||||
paragraph "`multiple` has no effect because you can only choose a single icon"
|
||||
}
|
||||
section("required: true") {
|
||||
input(type: "icon", name: "iconRequired", title: "without description", required: true)
|
||||
input(type: "icon", name: "iconRequiredWithDescription", title: "with description", description: "this is a description", required: true)
|
||||
}
|
||||
section("with image") {
|
||||
paragraph "The image specified will be replaced after an icon is selected"
|
||||
input(type: "icon", name: "iconwithImage", title: "without description", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
}
|
||||
}
|
||||
def inputImagePage() {
|
||||
dynamicPage(name: "inputImagePage") {
|
||||
section {
|
||||
paragraph "This only exists in DeviceTypes. Someone should do something about that. (glares at MikeDave)"
|
||||
paragraph "Go to the device preferences of a Mobile Presence device to see it in action"
|
||||
paragraph "If you try to set the value of this, it will not behave as it would in Device Preferences"
|
||||
input(type: "image", title: "This is kind of what it looks like", required: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def optionsGroup(List groups, String title) {
|
||||
def group = [values:[], order: groups.size()]
|
||||
group.title = title ?: ""
|
||||
groups << group
|
||||
return groups
|
||||
}
|
||||
def addValues(List groups, String key, String value) {
|
||||
def lastGroup = groups[-1]
|
||||
lastGroup["values"] << [
|
||||
key: key,
|
||||
value: value,
|
||||
order: lastGroup["values"].size()
|
||||
]
|
||||
return groups
|
||||
}
|
||||
def listToMap(List original) {
|
||||
original.inject([:]) { result, v ->
|
||||
result[v] = v
|
||||
return result
|
||||
}
|
||||
}
|
||||
def addGroup(List groups, String title, values) {
|
||||
if (values instanceof List) {
|
||||
values = listToMap(values)
|
||||
}
|
||||
|
||||
values.inject(optionsGroup(groups, title)) { result, k, v ->
|
||||
return addValues(result, k, v)
|
||||
}
|
||||
return groups
|
||||
}
|
||||
def addGroup(values) {
|
||||
addGroup([], null, values)
|
||||
}
|
||||
/* Example usage of options builder
|
||||
|
||||
// Creating grouped options
|
||||
def newGroups = []
|
||||
addGroup(newGroups, "first group", ["foo", "bar", "baz"])
|
||||
addGroup(newGroups, "second group", [zero: "zero", one: "uno", two: "dos", three: "tres"])
|
||||
|
||||
// simple list
|
||||
addGroup(["a", "b", "c"])
|
||||
|
||||
// simple map
|
||||
addGroup(["a": "yes", "b": "no", "c": "maybe"])
|
||||
*/
|
||||
|
||||
|
||||
def inputSelectionPage() {
|
||||
|
||||
def englishOptions = ["One", "Two", "Three"]
|
||||
def spanishOptions = ["Uno", "Dos", "Tres"]
|
||||
def groupedOptions = []
|
||||
addGroup(groupedOptions, "English", englishOptions)
|
||||
addGroup(groupedOptions, "Spanish", spanishOptions)
|
||||
|
||||
dynamicPage(name: "inputSelectionPage") {
|
||||
|
||||
section("options variations") {
|
||||
paragraph "tap these elements and look at the differences when selecting an option"
|
||||
input(type: "enum", name: "selectionSimple", title: "Simple options", description: "no separators in the selectable options", groupedOptions: addGroup(englishOptions + spanishOptions))
|
||||
input(type: "enum", name: "selectionGrouped", title: "Grouped options", description: "separate groups of options with headers", groupedOptions: groupedOptions)
|
||||
}
|
||||
|
||||
section("list vs map") {
|
||||
paragraph "These should be identical in UI, but are different in code and will produce different settings"
|
||||
input(type: "enum", name: "selectionList", title: "Choose a device", description: "settings will be something like ['Device1 Label']", groupedOptions: addGroup(["Device1 Label", "Device2 Label"]))
|
||||
input(type: "enum", name: "selectionMap", title: "Choose a device", description: "settings will be something like ['device1-id']", groupedOptions: addGroup(["device1-id": "Device1 Label", "device2-id": "Device2 Label"]))
|
||||
}
|
||||
|
||||
section("segmented") {
|
||||
paragraph "segmented should only work if there are either 2 or 3 options to choose from"
|
||||
input(type: "enum", name: "selectionSegmented1", style: "segmented", title: "1 option", groupedOptions: addGroup(["One"]))
|
||||
input(type: "enum", name: "selectionSegmented4", style: "segmented", title: "4 options", groupedOptions: addGroup(["One", "Two", "Three", "Four"]))
|
||||
|
||||
paragraph "multiple and required will have no effect on segmented selection elements. There will always be exactly 1 option selected"
|
||||
input(type: "enum", name: "selectionSegmented2", style: "segmented", title: "2 options", options: ["One", "Two"])
|
||||
input(type: "enum", name: "selectionSegmented3", style: "segmented", title: "3 options", options: ["One", "Two", "Three"])
|
||||
|
||||
paragraph "specifying defaultValue still works with segmented selection elements"
|
||||
input(type: "enum", name: "selectionSegmentedWithDefault", title: "defaulted to 'two'", groupedOptions: addGroup(["One", "Two", "Three"]), defaultValue: "Two")
|
||||
}
|
||||
|
||||
section("required: true") {
|
||||
input(type: "enum", name: "selectionRequired", title: "This is required", description: "It should look different when nothing is selected", groupedOptions: addGroup(["only option"]), required: true)
|
||||
}
|
||||
|
||||
section("multiple: true") {
|
||||
input(type: "enum", name: "selectionMultiple", title: "This allows multiple selections", description: "It should look different when nothing is selected", groupedOptions: addGroup(["an option", "another option", "no way, one more?"]), multiple: true)
|
||||
}
|
||||
|
||||
section("with image") {
|
||||
input(type: "enum", name: "selectionWithImage", title: "This has an image", description: "and a description", groupedOptions: addGroup(["an option", "another option", "no way, one more?"]), image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
}
|
||||
}
|
||||
def inputTextPage() {
|
||||
dynamicPage(name: "inputTextPage", title: "Every 'text' variation") {
|
||||
section("style and functional differences") {
|
||||
input(type: "text", name: "textRequired", title: "required: true", description: "This should look different when nothing has been entered", required: true)
|
||||
input(type: "text", name: "textWithImage", title: "with image", description: "This should look different when nothing has been entered", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", required: false)
|
||||
}
|
||||
section("text") {
|
||||
input(type: "text", name: "text", title: "This has an alpha-numeric keyboard", description: "no special formatting", required: false)
|
||||
}
|
||||
section("password") {
|
||||
input(type: "password", name: "password", title: "This has an alpha-numeric keyboard", description: "masks value", required: false)
|
||||
}
|
||||
section("email") {
|
||||
input(type: "email", name: "email", title: "This has an email-specific keyboard", description: "no special formatting", required: false)
|
||||
}
|
||||
section("phone") {
|
||||
input(type: "phone", name: "phone", title: "This has a numeric keyboard", description: "formatted for phone numbers", required: false)
|
||||
}
|
||||
section("decimal") {
|
||||
input(type: "decimal", name: "decimal", title: "This has an numeric keyboard with decimal point", description: "no special formatting", required: false)
|
||||
}
|
||||
section("number") {
|
||||
input(type: "number", name: "number", title: "This has an numeric keyboard without decimal point", description: "no special formatting", required: false)
|
||||
}
|
||||
|
||||
section("specified ranges") {
|
||||
paragraph "You can limit number and decimal inputs to a specific range."
|
||||
input(range: "50..150", type: "decimal", name: "decimalRange50..150", title: "only values between 50 and 150 will pass validation", description: "no special formatting", required: false)
|
||||
paragraph "Negative limits will add a negative symbol to the keyboard."
|
||||
input(range: "-50..50", type: "number", name: "numberRange-50..50", title: "only values between -50 and 50 will pass validation", description: "no special formatting", required: false)
|
||||
paragraph "Specify * to not limit one side or the other."
|
||||
input(range: "*..0", type: "decimal", name: "decimalRange*..0", title: "only negative values will pass validation", description: "no special formatting", required: false)
|
||||
input(range: "*..*", type: "number", name: "numberRange*..*", title: "only positive values will pass validation", description: "no special formatting", required: false)
|
||||
paragraph "If you don't specify a range, it defaults to 0..*"
|
||||
}
|
||||
}
|
||||
}
|
||||
def inputTimePage() {
|
||||
dynamicPage(name: "inputTimePage") {
|
||||
section {
|
||||
input(type: "time", name: "timeWithDescription", title: "a time picker", description: "with a description", required: false)
|
||||
input(type: "time", name: "timeWithoutDescription", title: "without a description", description: null, required: false)
|
||||
input(type: "time", name: "timeRequired", title: "required: true", required: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// selection subsets
|
||||
def inputDevicePage() {
|
||||
|
||||
dynamicPage(name: "inputDevicePage") {
|
||||
|
||||
section("required: true") {
|
||||
input(type: "device.switch", name: "deviceRequired", title: "This is required", description: "It should look different when nothing is selected")
|
||||
}
|
||||
|
||||
section("multiple: true") {
|
||||
input(type: "device.switch", name: "deviceMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
|
||||
}
|
||||
|
||||
section("with image") {
|
||||
input(type: "device.switch", name: "deviceRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
}
|
||||
}
|
||||
def inputCapabilityPage() {
|
||||
|
||||
dynamicPage(name: "inputCapabilityPage") {
|
||||
|
||||
section("required: true") {
|
||||
input(type: "capability.switch", name: "capabilityRequired", title: "This is required", description: "It should look different when nothing is selected")
|
||||
}
|
||||
|
||||
section("multiple: true") {
|
||||
input(type: "capability.switch", name: "capabilityMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
|
||||
}
|
||||
|
||||
section("with image") {
|
||||
input(type: "capability.switch", name: "capabilityRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
}
|
||||
}
|
||||
def inputRoomPage() {
|
||||
|
||||
dynamicPage(name: "inputRoomPage") {
|
||||
|
||||
section("required: true") {
|
||||
input(type: "room", name: "roomRequired", title: "This is required", description: "It should look different when nothing is selected")
|
||||
}
|
||||
|
||||
section("multiple: true") {
|
||||
input(type: "room", name: "roomMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
|
||||
}
|
||||
|
||||
section("with image") {
|
||||
input(type: "room", name: "roomRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
}
|
||||
}
|
||||
def inputModePage() {
|
||||
|
||||
dynamicPage(name: "inputModePage") {
|
||||
|
||||
section("required: true") {
|
||||
input(type: "mode", name: "modeRequired", title: "This is required", description: "It should look different when nothing is selected")
|
||||
}
|
||||
|
||||
section("multiple: true") {
|
||||
input(type: "mode", name: "modeMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
|
||||
}
|
||||
|
||||
section("with image") {
|
||||
input(type: "mode", name: "modeRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
}
|
||||
}
|
||||
def inputHubPage() {
|
||||
|
||||
dynamicPage(name: "inputHubPage") {
|
||||
|
||||
section("required: true") {
|
||||
input(type: "hub", name: "hubRequired", title: "This is required", description: "It should look different when nothing is selected")
|
||||
}
|
||||
|
||||
section("multiple: true") {
|
||||
input(type: "hub", name: "hubMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
|
||||
}
|
||||
|
||||
section("with image") {
|
||||
input(type: "hub", name: "hubRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
}
|
||||
}
|
||||
def inputContactBookPage() {
|
||||
|
||||
dynamicPage(name: "inputContactBookPage") {
|
||||
|
||||
section("required: true") {
|
||||
input(type: "contact", name: "contactRequired", title: "This is required", description: "It should look different when nothing is selected")
|
||||
}
|
||||
|
||||
section("multiple: true") {
|
||||
input(type: "contact", name: "contactMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true)
|
||||
}
|
||||
|
||||
section("with image") {
|
||||
input(type: "contact", name: "contactRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def appPage() {
|
||||
dynamicPage(name: "appPage", title: "Every 'app' type") {
|
||||
section {
|
||||
paragraph "These won't work unless you create a child SmartApp to link to... Sorry."
|
||||
}
|
||||
section("app") {
|
||||
app(
|
||||
name: "app",
|
||||
title: "required:false, multiple:false",
|
||||
required: false,
|
||||
multiple: false,
|
||||
namespace: "Steve",
|
||||
appName: "Child SmartApp"
|
||||
)
|
||||
app(name: "appRequired", title: "required:true", required: true, multiple: false, namespace: "Steve", appName: "Child SmartApp")
|
||||
app(name: "appComplete", title: "state:complete", required: false, multiple: false, namespace: "Steve", appName: "Child SmartApp", state: "complete")
|
||||
app(name: "appWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", namespace: "Steve", appName: "Child SmartApp")
|
||||
}
|
||||
section("multiple:true") {
|
||||
app(name: "appMultiple", title: "multiple:true", required: false, multiple: true, namespace: "Steve", appName: "Child SmartApp")
|
||||
}
|
||||
section("multiple:true with image") {
|
||||
app(name: "appMultipleWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", namespace: "Steve", appName: "Child SmartApp")
|
||||
}
|
||||
}
|
||||
dynamicPage(name: "appPage", title: "Every 'app' type") {
|
||||
section {
|
||||
paragraph "These won't work unless you create a child SmartApp to link to... Sorry."
|
||||
}
|
||||
section("app") {
|
||||
app(
|
||||
name: "app",
|
||||
title: "required:false, multiple:false",
|
||||
required: false,
|
||||
multiple: false,
|
||||
namespace: "Steve",
|
||||
appName: "Child SmartApp"
|
||||
)
|
||||
app(name: "appRequired", title: "required:true", required: true, multiple: false, namespace: "Steve", appName: "Child SmartApp")
|
||||
app(name: "appComplete", title: "state:complete", required: false, multiple: false, namespace: "Steve", appName: "Child SmartApp", state: "complete")
|
||||
app(name: "appWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", namespace: "Steve", appName: "Child SmartApp")
|
||||
}
|
||||
section("multiple:true") {
|
||||
app(name: "appMultiple", title: "multiple:true", required: false, multiple: true, namespace: "Steve", appName: "Child SmartApp")
|
||||
}
|
||||
section("multiple:true with image") {
|
||||
app(name: "appMultipleWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", namespace: "Steve", appName: "Child SmartApp")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def labelPage() {
|
||||
dynamicPage(name: "labelPage", title: "Every 'Label' type") {
|
||||
section("label") {
|
||||
label(name: "label", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
label(name: "labelRequired", title: "required:true", required: true, multiple: false)
|
||||
label(name: "labelWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
}
|
||||
dynamicPage(name: "labelPage", title: "Every 'Label' type") {
|
||||
section("label") {
|
||||
paragraph "The difference between a label element and a text input element is that the label element will effect the SmartApp directly by setting the label. An input element will place the set value in the SmartApp's settings."
|
||||
paragraph "There are 3 here as an example. Never use more than 1 label element on a page."
|
||||
label(name: "label", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
label(name: "labelRequired", title: "required:true", required: true, multiple: false)
|
||||
label(name: "labelWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def modePage() {
|
||||
dynamicPage(name: "modePage", title: "Every 'mode' type") { // TODO: finish this
|
||||
section("mode") {
|
||||
mode(name: "mode", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
mode(name: "modeRequired", title: "required:true", required: true, multiple: false)
|
||||
mode(name: "modeMultiple", title: "multiple:true", required: false, multiple: true)
|
||||
mode(name: "modeWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
}
|
||||
dynamicPage(name: "modePage", title: "Every 'mode' type") { // TODO: finish this
|
||||
section("mode") {
|
||||
paragraph "The difference between a mode element and a mode input element is that the mode element will effect the SmartApp directly by setting the modes it executes in. A mode input element will place the set value in the SmartApp's settings."
|
||||
paragraph "Another difference is that you can select 'All Modes' when choosing which mode the SmartApp should execute in. This is the same as selecting no modes. When a SmartApp does not have modes specified, it will execute in all modes."
|
||||
paragraph "There are 4 here as an example. Never use more than 1 mode element on a page."
|
||||
mode(name: "mode", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
mode(name: "modeRequired", title: "required:true", required: true, multiple: false)
|
||||
mode(name: "modeMultiple", title: "multiple:true", required: false, multiple: true)
|
||||
mode(name: "modeWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def paragraphPage() {
|
||||
dynamicPage(name: "paragraphPage", title: "Every 'paragraph' type") {
|
||||
section("paragraph") {
|
||||
paragraph "This us how you should make a paragraph element"
|
||||
paragraph image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", "This is a long description, blah, blah, blah."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def iconPage() {
|
||||
dynamicPage(name: "iconPage", title: "Every 'icon' type") { // TODO: finish this
|
||||
section("icon") {
|
||||
icon(name: "icon", title: "required:false, multiple:false", required: false, multiple: false)
|
||||
icon(name: "iconRequired", title: "required:true", required: true, multiple: false)
|
||||
icon(name: "iconWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
}
|
||||
dynamicPage(name: "paragraphPage", title: "Every 'paragraph' type") {
|
||||
section("paragraph") {
|
||||
paragraph "This is how you should make a paragraph element"
|
||||
paragraph image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", "This is a long description, blah, blah, blah."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def hrefPage() {
|
||||
dynamicPage(name: "hrefPage", title: "Every 'href' type") {
|
||||
section("page") {
|
||||
href(name: "hrefPage", title: "required:false, multiple:false", required: false, multiple: false, page: "deadEnd")
|
||||
href(name: "hrefPageRequired", title: "required:true", required: true, multiple: false, page: "deadEnd", description: "Don't make hrefs required")
|
||||
href(name: "hrefPageComplete", title: "state:complete", required: false, multiple: false, page: "deadEnd", state: "complete")
|
||||
href(name: "hrefPageWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", page: "deadEnd",)
|
||||
}
|
||||
section("external") {
|
||||
href(name: "hrefExternal", title: "required:false, multiple:false", required: false, multiple: false, style: "external", url: "http://smartthings.com/")
|
||||
href(name: "hrefExternalRequired", title: "required:true", required: true, multiple: false, style: "external", url: "http://smartthings.com/", description: "Don't make hrefs required")
|
||||
href(name: "hrefExternalComplete", title: "state:complete", required: false, multiple: true, style: "external", url: "http://smartthings.com/", state: "complete")
|
||||
href(name: "hrefExternalWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", url: "http://smartthings.com/")
|
||||
}
|
||||
section("embedded") {
|
||||
href(name: "hrefEmbedded", title: "required:false, multiple:false", required: false, multiple: false, style: "embedded", url: "http://smartthings.com/")
|
||||
href(name: "hrefEmbeddedRequired", title: "required:true", required: true, multiple: false, style: "embedded", url: "http://smartthings.com/", description: "Don't make hrefs required")
|
||||
href(name: "hrefEmbeddedComplete", title: "state:complete", required: false, multiple: true, style: "embedded", url: "http://smartthings.com/", state: "complete")
|
||||
href(name: "hrefEmbeddedWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", url: "http://smartthings.com/")
|
||||
}
|
||||
}
|
||||
dynamicPage(name: "hrefPage", title: "Every 'href' variation") {
|
||||
section("stylistic differences") {
|
||||
href(page: "deadEnd", title: "state: 'complete'", description: "gives the appearance of an input that has been filled out", state: "complete")
|
||||
href(page: "deadEnd", title: "with image", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
href(page: "deadEnd", title: "with image and description", description: "and state: 'complete'", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", state: "complete")
|
||||
}
|
||||
section("functional differences") {
|
||||
href(page: "deadEnd", title: "to a page within the app")
|
||||
href(url: "http://www.google.com", title: "to a url using all defaults")
|
||||
href(url: "http://www.google.com", title: "external: true", description: "takes you outside the app", external: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def buttonsPage() {
|
||||
dynamicPage(name: "buttonsPage", title: "Every 'button' type") {
|
||||
section("buttons") {
|
||||
buttons(name: "buttons", title: "required:false, multiple:false", required: false, multiple: false, buttons: [
|
||||
[label: "foo", action: "foo"],
|
||||
[label: "bar", action: "bar"]
|
||||
])
|
||||
buttons(name: "buttonsRequired", title: "required:true", required: true, multiple: false, buttons: [
|
||||
[label: "foo", action: "foo"],
|
||||
[label: "bar", action: "bar"]
|
||||
])
|
||||
buttons(name: "buttonsWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", buttons: [
|
||||
[label: "foo", action: "foo"],
|
||||
[label: "bar", action: "bar"]
|
||||
])
|
||||
}
|
||||
section("Colored Buttons") {
|
||||
buttons(name: "buttonsColoredSpecial", title: "special strings", description: "SmartThings highly recommends using these colors", buttons: [
|
||||
[label: "complete", action: "bar", backgroundColor: "complete"],
|
||||
[label: "required", action: "bar", backgroundColor: "required"]
|
||||
])
|
||||
buttons(name: "buttonsColoredHex", title: "hex values work", buttons: [
|
||||
[label: "bg: #000dff", action: "foo", backgroundColor: "#000dff"],
|
||||
[label: "fg: #ffac00", action: "foo", color: "#ffac00"],
|
||||
[label: "both fg and bg", action: "foo", color: "#ffac00", backgroundColor: "#000dff"]
|
||||
])
|
||||
buttons(name: "buttonsColoredString", title: "strings work too", buttons: [
|
||||
[label: "green", action: "foo", backgroundColor: "green"],
|
||||
[label: "red", action: "foo", backgroundColor: "red"],
|
||||
[label: "both fg and bg", action: "foo", color: "red", backgroundColor: "green"]
|
||||
])
|
||||
}
|
||||
}
|
||||
dynamicPage(name: "buttonsPage", title: "Every 'button' type") {
|
||||
section("Simple Buttons") {
|
||||
paragraph "If there are an odd number of buttons, the last button will span the entire view area."
|
||||
buttons(name: "buttons1", title: "1 button", buttons: [
|
||||
[label: "foo", action: "foo"]
|
||||
])
|
||||
buttons(name: "buttons2", title: "2 buttons", buttons: [
|
||||
[label: "foo", action: "foo"],
|
||||
[label: "bar", action: "bar"]
|
||||
])
|
||||
buttons(name: "buttons3", title: "3 buttons", buttons: [
|
||||
[label: "foo", action: "foo"],
|
||||
[label: "bar", action: "bar"],
|
||||
[label: "baz", action: "baz"]
|
||||
])
|
||||
buttons(name: "buttonsWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", buttons: [
|
||||
[label: "foo", action: "foo"],
|
||||
[label: "bar", action: "bar"]
|
||||
])
|
||||
}
|
||||
section("Colored Buttons") {
|
||||
buttons(name: "buttonsColoredSpecial", title: "special strings", description: "SmartThings highly recommends using these colors", buttons: [
|
||||
[label: "complete", action: "bar", backgroundColor: "complete"],
|
||||
[label: "required", action: "bar", backgroundColor: "required"]
|
||||
])
|
||||
buttons(name: "buttonsColoredHex", title: "hex values work", buttons: [
|
||||
[label: "bg: #000dff", action: "foo", backgroundColor: "#000dff"],
|
||||
[label: "fg: #ffac00", action: "foo", color: "#ffac00"],
|
||||
[label: "both fg and bg", action: "foo", color: "#ffac00", backgroundColor: "#000dff"]
|
||||
])
|
||||
buttons(name: "buttonsColoredString", title: "strings work too", buttons: [
|
||||
[label: "green", action: "foo", backgroundColor: "green"],
|
||||
[label: "red", action: "foo", backgroundColor: "red"],
|
||||
[label: "both fg and bg", action: "foo", color: "red", backgroundColor: "green"]
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def imagePage() {
|
||||
dynamicPage(name: "imagePage", title: "Every 'image' type") { // TODO: finish thise
|
||||
section("image") {
|
||||
image "http://f.cl.ly/items/1k1S0A0m3805402o3O12/20130915-191127.jpg"
|
||||
image(name: "imageWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png")
|
||||
}
|
||||
}
|
||||
dynamicPage(name: "imagePage", title: "Every 'image' type") { // TODO: finish thise
|
||||
section("image") {
|
||||
image "http://f.cl.ly/items/1k1S0A0m3805402o3O12/20130915-191127.jpg"
|
||||
image(name: "imageWithMultipleImages", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, images: ["https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", "http://f.cl.ly/items/1k1S0A0m3805402o3O12/20130915-191127.jpg"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def videoPage() {
|
||||
dynamicPage(name: "imagePage", title: "Every 'image' type") { // TODO: finish this
|
||||
section("video") {
|
||||
// TODO: update this when there is a videoElement method
|
||||
element(name: "videoElement", element: "video", type: "video", title: "this is a video!", description: "I am setting long title and descriptions to test the offset", required: false, image: "http://ec2-54-161-144-215.compute-1.amazonaws.com:8081/jesse/cam1/54aafcd1c198347511c26321.jpg", video: "http://ec2-54-161-144-215.compute-1.amazonaws.com:8081/jesse/cam1/54aafcd1c198347511c2631f.mp4")
|
||||
}
|
||||
}
|
||||
dynamicPage(name: "videoPage", title: "Every 'video' type") { // TODO: finish this
|
||||
section("video") {
|
||||
// TODO: update this when there is a videoElement method
|
||||
element(name: "videoElement", element: "video", type: "video", title: "this is a video!", description: "I am setting long title and descriptions to test the offset", required: false, image: "http://f.cl.ly/items/0w0D1p0K2D0d190F3H3N/Image%202015-12-14%20at%207.57.27%20AM.jpg", video: "http://f.cl.ly/items/3O2L03471l2K3E3l3K1r/Zombie%20Kid%20Likes%20Turtles.mp4")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def flattenedPage() {
|
||||
def allSections = []
|
||||
firstPage().sections.each { section ->
|
||||
section.body.each { hrefElement ->
|
||||
if (hrefElement.page != "flattenedPage") {
|
||||
allSections += "${hrefElement.page}"().sections
|
||||
}
|
||||
}
|
||||
}
|
||||
def flattenedPage = dynamicPage(name: "flattenedPage", title: "All elements in one page!") {}
|
||||
flattenedPage.sections = allSections
|
||||
return flattenedPage
|
||||
def allSections = []
|
||||
firstPage().sections[0].body.each { hrefElement ->
|
||||
if (hrefElement.name != "inputPage") {
|
||||
// inputPage is a bunch of hrefs
|
||||
allSections += "${hrefElement.page}"().sections
|
||||
}
|
||||
}
|
||||
// collect the input elements
|
||||
inputPage().sections.each { section ->
|
||||
section.body.each { hrefElement ->
|
||||
allSections += "${hrefElement.page}"().sections
|
||||
}
|
||||
}
|
||||
def flattenedPage = dynamicPage(name: "flattenedPage", title: "All elements in one page!") {}
|
||||
flattenedPage.sections = allSections
|
||||
return flattenedPage
|
||||
}
|
||||
|
||||
def foo() {
|
||||
dynamicPage(name: "deadEnd") {
|
||||
|
||||
}
|
||||
dynamicPage(name: "deadEnd") {
|
||||
section { }
|
||||
}
|
||||
}
|
||||
|
||||
def installed() {
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
log.debug "Installed with settings: ${settings}"
|
||||
|
||||
initialize()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def updated() {
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
log.debug "Updated with settings: ${settings}"
|
||||
|
||||
unsubscribe()
|
||||
initialize()
|
||||
unsubscribe()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
// TODO: subscribe to attributes, devices, locations, etc.
|
||||
// TODO: subscribe to attributes, devices, locations, etc.
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ definition(
|
||||
description: "Connect and take pictures using your Foscam camera from inside the Smartthings app.",
|
||||
category: "SmartThings Internal",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam@2x.png"
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam@2x.png",
|
||||
singleInstance: true
|
||||
)
|
||||
|
||||
preferences {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2015 SmartThings
|
||||
* 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:
|
||||
@@ -38,37 +38,75 @@ preferences {
|
||||
page(name: "schedulingPage")
|
||||
page(name: "completionPage")
|
||||
page(name: "numbersPage")
|
||||
page(name: "controllerExplanationPage")
|
||||
}
|
||||
|
||||
def rootPage() {
|
||||
dynamicPage(name: "rootPage", title: "", install: true, uninstall: true) {
|
||||
|
||||
section {
|
||||
section("What to dim") {
|
||||
input(name: "dimmers", type: "capability.switchLevel", title: "Dimmers", description: null, multiple: true, required: true, submitOnChange: true)
|
||||
if (dimmers) {
|
||||
href(name: "toNumbersPage", page: "numbersPage", title: "Duration & Direction", description: numbersPageHrefDescription(), state: "complete")
|
||||
}
|
||||
}
|
||||
|
||||
if (dimmers) {
|
||||
|
||||
section {
|
||||
href(name: "toNumbersPage", page: "numbersPage", title: "Duration & Direction", description: numbersPageHrefDescription(), state: "complete")
|
||||
section("Gentle Wake Up Has A Controller") {
|
||||
href(title: "Learn how to control Gentle Wake Up", page: "controllerExplanationPage", description: null)
|
||||
}
|
||||
|
||||
section {
|
||||
href(name: "toSchedulingPage", page: "schedulingPage", title: "Rules For Automatically Dimming Your Lights", description: schedulingHrefDescription(), state: schedulingHrefDescription() ? "complete" : "")
|
||||
}
|
||||
|
||||
section {
|
||||
href(name: "toCompletionPage", title: "Completion Actions (Optional)", page: "completionPage", state: completionHrefDescription() ? "complete" : "", description: completionHrefDescription())
|
||||
section("Rules For Dimming") {
|
||||
href(name: "toSchedulingPage", page: "schedulingPage", title: "Automation", description: schedulingHrefDescription() ?: "Set rules for when to start", state: schedulingHrefDescription() ? "complete" : "")
|
||||
input(name: "manualOverride", type: "enum", options: ["cancel": "Cancel dimming", "jumpTo": "Jump to the end"], title: "When one of the dimmers is manually turned off…", description: "dimming will continue", required: false, multiple: false)
|
||||
href(name: "toCompletionPage", title: "Completion Actions", page: "completionPage", state: completionHrefDescription() ? "complete" : "", description: completionHrefDescription() ?: "Set rules for what to do when dimming completes")
|
||||
}
|
||||
|
||||
section {
|
||||
// TODO: fancy label
|
||||
label(title: "Label this SmartApp", required: false, defaultValue: "")
|
||||
label(title: "Label This SmartApp", required: false, defaultValue: "", description: "Highly recommended", submitOnChange: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def controllerExplanationPage() {
|
||||
dynamicPage(name: "controllerExplanationPage", title: "How To Control Gentle Wake Up") {
|
||||
|
||||
section("With other SmartApps", hideable: true, hidden: false) {
|
||||
paragraph "When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!"
|
||||
paragraph "The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!"
|
||||
paragraph "Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up."
|
||||
}
|
||||
|
||||
section("More about the controller", hideable: true, hidden: true) {
|
||||
paragraph "You can find the controller with your other 'Things'. It will look like this."
|
||||
image "http://f.cl.ly/items/2O0v0h41301U14042z3i/GentleWakeUpController-tile-stopped.png"
|
||||
paragraph "You can start and stop Gentle Wake up by tapping the control on the right."
|
||||
image "http://f.cl.ly/items/3W323J3M1b3K0k0V3X3a/GentleWakeUpController-tile-running.png"
|
||||
paragraph "If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls."
|
||||
image "http://f.cl.ly/items/291s3z2I2Q0r2q0x171H/GentleWakeUpController-richTile-stopped.png"
|
||||
paragraph "The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep."
|
||||
image "http://f.cl.ly/items/0F0N2G0S3v1q0L0R3J3Y/GentleWakeUpController-richTile-running.png"
|
||||
paragraph "In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle."
|
||||
paragraph "Of course, you may also tap the middle to start or stop the dimming cycle at any time."
|
||||
}
|
||||
|
||||
section("Starting and stopping the SmartApp itself", hideable: true, hidden: true) {
|
||||
paragraph "Tap the 'play' button on the SmartApp to start or stop dimming."
|
||||
image "http://f.cl.ly/items/0R2u1Z2H30393z2I2V3S/GentleWakeUp-appTouch2.png"
|
||||
}
|
||||
|
||||
section("Turning off devices while dimming", hideable: true, hidden: true) {
|
||||
paragraph "It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option."
|
||||
paragraph "If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings."
|
||||
paragraph "Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop."
|
||||
paragraph "That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def numbersPage() {
|
||||
dynamicPage(name:"numbersPage", title:"") {
|
||||
|
||||
@@ -128,24 +166,33 @@ def endLevelLabel() {
|
||||
return "${endLevel}%"
|
||||
}
|
||||
|
||||
def weekdays() {
|
||||
["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
|
||||
}
|
||||
|
||||
def weekends() {
|
||||
["Saturday", "Sunday"]
|
||||
}
|
||||
|
||||
def schedulingPage() {
|
||||
dynamicPage(name: "schedulingPage", title: "Rules For Automatically Dimming Your Lights") {
|
||||
|
||||
section {
|
||||
input(name: "days", type: "enum", title: "Allow Automatic Dimming On These Days", description: "Every day", required: false, multiple: true, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"])
|
||||
section("Use Other SmartApps!") {
|
||||
href(title: "Learn how to control Gentle Wake Up", page: "controllerExplanationPage", description: null)
|
||||
}
|
||||
|
||||
section {
|
||||
input(name: "modeStart", title: "Start when entering this mode", type: "mode", required: false, mutliple: false, submitOnChange: true)
|
||||
section("Allow Automatic Dimming") {
|
||||
input(name: "days", type: "enum", title: "On These Days", description: "Every day", required: false, multiple: true, options: weekdays() + weekends())
|
||||
}
|
||||
|
||||
section("Start Dimming...") {
|
||||
input(name: "startTime", type: "time", title: "At This Time", description: null, required: false)
|
||||
input(name: "modeStart", title: "When Entering This Mode", type: "mode", required: false, mutliple: false, submitOnChange: true, description: null)
|
||||
if (modeStart) {
|
||||
input(name: "modeStop", title: "Stop when leaving '${modeStart}' mode", type: "bool", required: false)
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
input(name: "startTime", type: "time", title: "Start Dimming At This Time", description: null, required: false)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,11 +241,16 @@ def updated() {
|
||||
log.debug "Updating 'Gentle Wake Up' with settings: ${settings}"
|
||||
unschedule()
|
||||
|
||||
def controller = getController()
|
||||
if (controller) {
|
||||
controller.label = app.label
|
||||
}
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
stop()
|
||||
stop("settingsChange")
|
||||
|
||||
if (startTime) {
|
||||
log.debug "scheduling dimming routine to run at $startTime"
|
||||
@@ -209,15 +261,27 @@ private initialize() {
|
||||
subscribe(app, appHandler)
|
||||
|
||||
subscribe(location, locationHandler)
|
||||
|
||||
if (manualOverride) {
|
||||
subscribe(dimmers, "switch.off", stopDimmersHandler)
|
||||
}
|
||||
|
||||
if (!getAllChildDevices()) {
|
||||
// create controller device and set name to the label used here
|
||||
def dni = "${new Date().getTime()}"
|
||||
log.debug "app.label: ${app.label}"
|
||||
addChildDevice("smartthings", "Gentle Wake Up Controller", dni, null, ["label": app.label])
|
||||
state.controllerDni = dni
|
||||
}
|
||||
}
|
||||
|
||||
def appHandler(evt) {
|
||||
log.debug "appHandler evt: ${evt.value}"
|
||||
if (evt.value == "touch") {
|
||||
if (atomicState.running) {
|
||||
stop()
|
||||
stop("appTouch")
|
||||
} else {
|
||||
start()
|
||||
start("appTouch")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,26 +297,47 @@ def locationHandler(evt) {
|
||||
def modeStopIsTrue = (modeStop && modeStop != "false")
|
||||
|
||||
if (isSpecifiedMode && canStartAutomatically()) {
|
||||
start()
|
||||
start("modeChange")
|
||||
} else if (!isSpecifiedMode && modeStopIsTrue) {
|
||||
stop()
|
||||
stop("modeChange")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def stopDimmersHandler(evt) {
|
||||
log.trace "stopDimmersHandler evt: ${evt.value}"
|
||||
def percentComplete = completionPercentage()
|
||||
// Often times, the first thing we do is turn lights on or off so make sure we don't stop as soon as we start
|
||||
if (percentComplete > 2 && percentComplete < 98) {
|
||||
if (manualOverride == "cancel") {
|
||||
log.debug "STOPPING in stopDimmersHandler"
|
||||
stop("manualOverride")
|
||||
} else if (manualOverride == "jumpTo") {
|
||||
def end = dynamicEndLevel()
|
||||
log.debug "Jumping to 99% complete in stopDimmersHandler"
|
||||
jumpTo(99)
|
||||
}
|
||||
|
||||
} else {
|
||||
log.debug "not stopping in stopDimmersHandler"
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// Scheduling
|
||||
// ========================================================
|
||||
|
||||
def scheduledStart() {
|
||||
if (canStartAutomatically()) {
|
||||
start()
|
||||
start("schedule")
|
||||
}
|
||||
}
|
||||
|
||||
def start() {
|
||||
public def start(source) {
|
||||
log.trace "START"
|
||||
|
||||
sendStartEvent(source)
|
||||
|
||||
setLevelsInState()
|
||||
|
||||
atomicState.running = true
|
||||
@@ -263,9 +348,11 @@ def start() {
|
||||
increment()
|
||||
}
|
||||
|
||||
def stop() {
|
||||
public def stop(source) {
|
||||
log.trace "STOP"
|
||||
|
||||
sendStopEvent(source)
|
||||
|
||||
atomicState.running = false
|
||||
atomicState.start = 0
|
||||
|
||||
@@ -282,6 +369,110 @@ private healthCheck() {
|
||||
increment()
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// Controller
|
||||
// ========================================================
|
||||
|
||||
def sendStartEvent(source) {
|
||||
log.trace "sendStartEvent(${source})"
|
||||
def eventData = [
|
||||
name: "sessionStatus",
|
||||
value: "running",
|
||||
descriptionText: "${app.label} has started dimming",
|
||||
displayed: true,
|
||||
linkText: app.label,
|
||||
isStateChange: true
|
||||
]
|
||||
if (source == "modeChange") {
|
||||
eventData.descriptionText += " because of a mode change"
|
||||
} else if (source == "schedule") {
|
||||
eventData.descriptionText += " as scheduled"
|
||||
} else if (source == "appTouch") {
|
||||
eventData.descriptionText += " because you pressed play on the app"
|
||||
} else if (source == "controller") {
|
||||
eventData.descriptionText += " because you pressed play on the controller"
|
||||
}
|
||||
|
||||
sendControllerEvent(eventData)
|
||||
}
|
||||
|
||||
def sendStopEvent(source) {
|
||||
log.trace "sendStopEvent(${source})"
|
||||
def eventData = [
|
||||
name: "sessionStatus",
|
||||
value: "stopped",
|
||||
descriptionText: "${app.label} has stopped dimming",
|
||||
displayed: true,
|
||||
linkText: app.label,
|
||||
isStateChange: true
|
||||
]
|
||||
if (source == "modeChange") {
|
||||
eventData.descriptionText += " because of a mode change"
|
||||
eventData.value += "cancelled"
|
||||
} else if (source == "schedule") {
|
||||
eventData.descriptionText = "${app.label} has finished dimming"
|
||||
} else if (source == "appTouch") {
|
||||
eventData.descriptionText += " because you pressed play on the app"
|
||||
eventData.value += "cancelled"
|
||||
} else if (source == "controller") {
|
||||
eventData.descriptionText += " because you pressed stop on the controller"
|
||||
eventData.value += "cancelled"
|
||||
} else if (source == "settingsChange") {
|
||||
eventData.descriptionText += " because the settings have changed"
|
||||
eventData.value += "cancelled"
|
||||
} else if (source == "manualOverride") {
|
||||
eventData.descriptionText += " because the dimmer was manually turned off"
|
||||
eventData.value += "cancelled"
|
||||
}
|
||||
|
||||
sendControllerEvent(eventData)
|
||||
sendTimeRemainingEvent(0)
|
||||
}
|
||||
|
||||
def sendTimeRemainingEvent(percentComplete) {
|
||||
log.trace "sendTimeRemainingEvent(${percentComplete})"
|
||||
|
||||
def percentCompleteEventData = [
|
||||
name: "percentComplete",
|
||||
value: percentComplete as int,
|
||||
displayed: true,
|
||||
isStateChange: true
|
||||
]
|
||||
sendControllerEvent(percentCompleteEventData)
|
||||
|
||||
def duration = sanitizeInt(duration, 30)
|
||||
def timeRemaining = duration - (duration * (percentComplete / 100))
|
||||
def timeRemainingEventData = [
|
||||
name: "timeRemaining",
|
||||
value: displayableTime(timeRemaining),
|
||||
displayed: true,
|
||||
isStateChange: true
|
||||
]
|
||||
sendControllerEvent(timeRemainingEventData)
|
||||
}
|
||||
|
||||
def sendControllerEvent(eventData) {
|
||||
def controller = getController()
|
||||
if (controller) {
|
||||
controller.controllerEvent(eventData)
|
||||
}
|
||||
}
|
||||
|
||||
def getController() {
|
||||
def dni = state.controllerDni
|
||||
if (!dni) {
|
||||
log.warn "no controller dni"
|
||||
return null
|
||||
}
|
||||
def controller = getChildDevice(dni)
|
||||
if (!controller) {
|
||||
log.warn "no controller"
|
||||
return null
|
||||
}
|
||||
log.debug "controller: ${controller}"
|
||||
return controller
|
||||
}
|
||||
|
||||
// ========================================================
|
||||
// Setting levels
|
||||
// ========================================================
|
||||
@@ -349,6 +540,8 @@ def updateDimmers(percentComplete) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
sendTimeRemainingEvent(percentComplete)
|
||||
}
|
||||
|
||||
int dynamicLevel(dimmer, percentComplete) {
|
||||
@@ -377,7 +570,7 @@ private completion() {
|
||||
return
|
||||
}
|
||||
|
||||
stop()
|
||||
stop("schedule")
|
||||
|
||||
handleCompletionSwitches()
|
||||
|
||||
@@ -385,6 +578,7 @@ private completion() {
|
||||
|
||||
handleCompletionModesAndPhrases()
|
||||
|
||||
sendTimeRemainingEvent(100)
|
||||
}
|
||||
|
||||
private handleCompletionSwitches() {
|
||||
@@ -493,22 +687,65 @@ def completionPercentage() {
|
||||
return
|
||||
}
|
||||
|
||||
int now = new Date().getTime()
|
||||
int diff = now - atomicState.start
|
||||
int totalRunTime = totalRunTimeMillis()
|
||||
int percentOfRunTime = (diff / totalRunTime) * 100
|
||||
log.debug "percentOfRunTime: ${percentOfRunTime}"
|
||||
def now = new Date().getTime()
|
||||
def timeElapsed = now - atomicState.start
|
||||
def totalRunTime = totalRunTimeMillis()
|
||||
def percentComplete = timeElapsed / totalRunTime * 100
|
||||
log.debug "percentComplete: ${percentComplete}"
|
||||
|
||||
percentOfRunTime
|
||||
return percentComplete
|
||||
}
|
||||
|
||||
int totalRunTimeMillis() {
|
||||
int minutes = sanitizeInt(duration, 30)
|
||||
convertToMillis(minutes)
|
||||
}
|
||||
|
||||
int convertToMillis(minutes) {
|
||||
def seconds = minutes * 60
|
||||
def millis = seconds * 1000
|
||||
return millis as int
|
||||
return millis
|
||||
}
|
||||
|
||||
def timeRemaining(percentComplete) {
|
||||
def normalizedPercentComplete = percentComplete / 100
|
||||
def duration = sanitizeInt(duration, 30)
|
||||
def timeElapsed = duration * normalizedPercentComplete
|
||||
def timeRemaining = duration - timeElapsed
|
||||
return timeRemaining
|
||||
}
|
||||
|
||||
int millisToEnd(percentComplete) {
|
||||
convertToMillis(timeRemaining(percentComplete))
|
||||
}
|
||||
|
||||
String displayableTime(timeRemaining) {
|
||||
def timeString = "${timeRemaining}"
|
||||
def parts = timeString.split(/\./)
|
||||
if (!parts.size()) {
|
||||
return "0:00"
|
||||
}
|
||||
def minutes = parts[0]
|
||||
if (parts.size() == 1) {
|
||||
return "${minutes}:00"
|
||||
}
|
||||
def fraction = "0.${parts[1]}" as double
|
||||
def seconds = "${60 * fraction as int}".padRight(2, "0")
|
||||
return "${minutes}:${seconds}"
|
||||
}
|
||||
|
||||
def jumpTo(percentComplete) {
|
||||
def millisToEnd = millisToEnd(percentComplete)
|
||||
def endTime = new Date().getTime() + millisToEnd
|
||||
def duration = sanitizeInt(duration, 30)
|
||||
def durationMillis = convertToMillis(duration)
|
||||
def shiftedStart = endTime - durationMillis
|
||||
atomicState.start = shiftedStart
|
||||
updateDimmers(percentComplete)
|
||||
sendTimeRemainingEvent(percentComplete)
|
||||
}
|
||||
|
||||
|
||||
int dynamicEndLevel() {
|
||||
if (usesOldSettings()) {
|
||||
if (direction && direction == "Down") {
|
||||
@@ -673,7 +910,13 @@ def schedulingHrefDescription() {
|
||||
|
||||
def descriptionParts = []
|
||||
if (days) {
|
||||
descriptionParts << "On ${fancyString(days)},"
|
||||
if (days == weekdays()) {
|
||||
descriptionParts << "On weekdays,"
|
||||
} else if (days == weekends()) {
|
||||
descriptionParts << "On weekends,"
|
||||
} else {
|
||||
descriptionParts << "On ${fancyString(days)},"
|
||||
}
|
||||
}
|
||||
|
||||
descriptionParts << "${fancyDeviceString(dimmers)} will start dimming"
|
||||
@@ -759,15 +1002,15 @@ def completionHrefDescription() {
|
||||
|
||||
def numbersPageHrefDescription() {
|
||||
def title = "All dimmers will dim for ${duration ?: '30'} minutes from ${startLevelLabel()} to ${endLevelLabel()}"
|
||||
if (colorize) {
|
||||
def colorDimmers = dimmersWithSetColorCommand()
|
||||
if (colorDimmers == dimmers) {
|
||||
title += " and will gradually change color."
|
||||
} else {
|
||||
title += ".\n${fancyDeviceString(colorDimmers)} will gradually change color."
|
||||
}
|
||||
}
|
||||
return title
|
||||
if (colorize) {
|
||||
def colorDimmers = dimmersWithSetColorCommand()
|
||||
if (colorDimmers == dimmers) {
|
||||
title += " and will gradually change color."
|
||||
} else {
|
||||
title += ".\n${fancyDeviceString(colorDimmers)} will gradually change color."
|
||||
}
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
def hueSatToHex(h, s) {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
definition(
|
||||
name: "Hue (Connect)",
|
||||
namespace: "smartthings",
|
||||
@@ -23,7 +23,8 @@ definition(
|
||||
description: "Allows you to connect your Philips Hue lights with SmartThings and control them from your Things area or Dashboard in the SmartThings Mobile app. Adjust colors by going to the Thing detail screen for your Hue lights (tap the gear on Hue tiles).\n\nPlease update your Hue Bridge first, outside of the SmartThings app, using the Philips Hue app.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png"
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png",
|
||||
singleInstance: true
|
||||
)
|
||||
|
||||
preferences {
|
||||
@@ -64,10 +65,14 @@ def bridgeDiscovery(params=[:])
|
||||
def options = bridges ?: []
|
||||
def numFound = options.size() ?: 0
|
||||
|
||||
if(!state.subscribe) {
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
state.subscribe = true
|
||||
}
|
||||
if (numFound == 0 && state.bridgeRefreshCount > 25) {
|
||||
log.trace "Cleaning old bridges memory"
|
||||
state.bridges = [:]
|
||||
state.bridgeRefreshCount = 0
|
||||
app.updateSetting("selectedHue", "")
|
||||
}
|
||||
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
|
||||
//bridge discovery request every 15 //25 seconds
|
||||
if((bridgeRefreshCount % 5) == 0) {
|
||||
@@ -94,11 +99,20 @@ def bridgeLinking()
|
||||
|
||||
def nextPage = ""
|
||||
def title = "Linking with your Hue"
|
||||
def paragraphText = "Press the button on your Hue Bridge to setup a link."
|
||||
def paragraphText
|
||||
def hueimage = null
|
||||
if (selectedHue) {
|
||||
paragraphText = "Press the button on your Hue Bridge to setup a link. "
|
||||
hueimage = "http://huedisco.mediavibe.nl/wp-content/uploads/2013/09/pair-bridge.png"
|
||||
} else {
|
||||
paragraphText = "You haven't selected a Hue Bridge, please Press \"Done\" and select one before clicking next."
|
||||
hueimage = null
|
||||
}
|
||||
if (state.username) { //if discovery worked
|
||||
nextPage = "bulbDiscovery"
|
||||
title = "Success! - click 'Next'"
|
||||
title = "Success!"
|
||||
paragraphText = "Linking to your hub was a success! Please click 'Next'!"
|
||||
hueimage = null
|
||||
}
|
||||
|
||||
if((linkRefreshcount % 2) == 0 && !state.username) {
|
||||
@@ -106,30 +120,39 @@ def bridgeLinking()
|
||||
}
|
||||
|
||||
return dynamicPage(name:"bridgeBtnPush", title:title, nextPage:nextPage, refreshInterval:refreshInterval) {
|
||||
section("Button Press") {
|
||||
section("") {
|
||||
paragraph """${paragraphText}"""
|
||||
if (hueimage != null)
|
||||
image "${hueimage}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def bulbDiscovery()
|
||||
{
|
||||
def bulbDiscovery() {
|
||||
int bulbRefreshCount = !state.bulbRefreshCount ? 0 : state.bulbRefreshCount as int
|
||||
state.bulbRefreshCount = bulbRefreshCount + 1
|
||||
def refreshInterval = 3
|
||||
|
||||
def options = bulbsDiscovered() ?: []
|
||||
def numFound = options.size() ?: 0
|
||||
|
||||
if((bulbRefreshCount % 3) == 0) {
|
||||
state.inBulbDiscovery = true
|
||||
def bridge = null
|
||||
if (selectedHue) {
|
||||
bridge = getChildDevice(selectedHue)
|
||||
subscribe(bridge, "bulbList", bulbListData)
|
||||
}
|
||||
state.bridgeRefreshCount = 0
|
||||
def bulboptions = bulbsDiscovered() ?: [:]
|
||||
def numFound = bulboptions.size() ?: 0
|
||||
if (numFound == 0)
|
||||
app.updateSetting("selectedBulbs", "")
|
||||
|
||||
if((bulbRefreshCount % 5) == 0) {
|
||||
discoverHueBulbs()
|
||||
}
|
||||
|
||||
return dynamicPage(name:"bulbDiscovery", title:"Bulb Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) {
|
||||
section("Please wait while we discover your Hue Bulbs. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||
input "selectedBulbs", "enum", required:false, title:"Select Hue Bulbs (${numFound} found)", multiple:true, options:options
|
||||
input "selectedBulbs", "enum", required:false, title:"Select Hue Bulbs (${numFound} found)", multiple:true, options:bulboptions
|
||||
}
|
||||
section {
|
||||
section {
|
||||
def title = getBridgeIP() ? "Hue bridge (${getBridgeIP()})" : "Find bridges"
|
||||
href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true]
|
||||
|
||||
@@ -194,24 +217,29 @@ Map bridgesDiscovered() {
|
||||
|
||||
Map bulbsDiscovered() {
|
||||
def bulbs = getHueBulbs()
|
||||
def map = [:]
|
||||
def bulbmap = [:]
|
||||
if (bulbs instanceof java.util.Map) {
|
||||
bulbs.each {
|
||||
def value = "${it?.value?.name}"
|
||||
def key = app.id +"/"+ it?.value?.id
|
||||
map["${key}"] = value
|
||||
def value = "${it.value.name}"
|
||||
def key = app.id +"/"+ it.value.id
|
||||
bulbmap["${key}"] = value
|
||||
}
|
||||
} else { //backwards compatable
|
||||
bulbs.each {
|
||||
def value = "${it?.name}"
|
||||
def key = app.id +"/"+ it?.id
|
||||
map["${key}"] = value
|
||||
def value = "${it.name}"
|
||||
def key = app.id +"/"+ it.id
|
||||
logg += "$value - $key, "
|
||||
bulbmap["${key}"] = value
|
||||
}
|
||||
}
|
||||
map
|
||||
return bulbmap
|
||||
}
|
||||
|
||||
def getHueBulbs() {
|
||||
def bulbListData(evt) {
|
||||
state.bulbs = evt.jsonData
|
||||
}
|
||||
|
||||
Map getHueBulbs() {
|
||||
state.bulbs = state.bulbs ?: [:]
|
||||
}
|
||||
|
||||
@@ -230,25 +258,20 @@ def installed() {
|
||||
|
||||
def updated() {
|
||||
log.trace "Updated with settings: ${settings}"
|
||||
unschedule()
|
||||
unsubscribe()
|
||||
unsubscribe()
|
||||
unschedule()
|
||||
initialize()
|
||||
}
|
||||
|
||||
def initialize() {
|
||||
log.debug "Initializing"
|
||||
state.subscribe = false
|
||||
state.bridgeSelectedOverride = false
|
||||
def bridge = null
|
||||
|
||||
log.debug "Initializing"
|
||||
unsubscribe(bridge)
|
||||
state.inBulbDiscovery = false
|
||||
state.bridgeRefreshCount = 0
|
||||
state.bulbRefreshCount = 0
|
||||
if (selectedHue) {
|
||||
addBridge()
|
||||
bridge = getChildDevice(selectedHue)
|
||||
subscribe(bridge, "bulbList", bulbListHandler)
|
||||
}
|
||||
|
||||
if (selectedBulbs) {
|
||||
addBulbs()
|
||||
addBridge()
|
||||
addBulbs()
|
||||
doDeviceSync()
|
||||
runEvery5Minutes("doDeviceSync")
|
||||
}
|
||||
@@ -263,44 +286,54 @@ def manualRefresh() {
|
||||
|
||||
def uninstalled(){
|
||||
state.bridges = [:]
|
||||
state.subscribe = false
|
||||
state.username = null
|
||||
}
|
||||
|
||||
// Handles events to add new bulbs
|
||||
def bulbListHandler(evt) {
|
||||
def bulbs = [:]
|
||||
log.trace "Adding bulbs to state..."
|
||||
state.bridgeProcessedLightList = true
|
||||
evt.jsonData.each { k,v ->
|
||||
log.trace "$k: $v"
|
||||
if (v instanceof Map) {
|
||||
bulbs[k] = [id: k, name: v.name, type: v.type, hub:evt.value]
|
||||
}
|
||||
}
|
||||
state.bulbs = bulbs
|
||||
log.info "${bulbs.size()} bulbs found"
|
||||
def bulbListHandler(hub, data = "") {
|
||||
def msg = "Bulbs list not processed. Only while in settings menu."
|
||||
def bulbs = [:]
|
||||
if (state.inBulbDiscovery) {
|
||||
def logg = ""
|
||||
log.trace "Adding bulbs to state..."
|
||||
state.bridgeProcessedLightList = true
|
||||
def object = new groovy.json.JsonSlurper().parseText(data)
|
||||
object.each { k,v ->
|
||||
if (v instanceof Map)
|
||||
bulbs[k] = [id: k, name: v.name, type: v.type, hub:hub]
|
||||
}
|
||||
}
|
||||
def bridge = null
|
||||
if (selectedHue)
|
||||
bridge = getChildDevice(selectedHue)
|
||||
bridge.sendEvent(name: "bulbList", value: hub, data: bulbs, isStateChange: true, displayed: false)
|
||||
msg = "${bulbs.size()} bulbs found. ${bulbs}"
|
||||
return msg
|
||||
}
|
||||
|
||||
def addBulbs() {
|
||||
def bulbs = getHueBulbs()
|
||||
selectedBulbs.each { dni ->
|
||||
selectedBulbs?.each { dni ->
|
||||
def d = getChildDevice(dni)
|
||||
if(!d) {
|
||||
def newHueBulb
|
||||
if (bulbs instanceof java.util.Map) {
|
||||
newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni }
|
||||
if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light")) {
|
||||
d = addChildDevice("smartthings", "Hue Lux Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
||||
} else {
|
||||
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
||||
}
|
||||
} else {
|
||||
if (newHueBulb != null) {
|
||||
if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light") ) {
|
||||
d = addChildDevice("smartthings", "Hue Lux Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
||||
} else {
|
||||
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name])
|
||||
}
|
||||
log.debug "created ${d.displayName} with id $dni"
|
||||
} else {
|
||||
log.debug "$dni in not longer paired to the Hue Bridge or ID changed"
|
||||
}
|
||||
} else {
|
||||
//backwards compatable
|
||||
newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni }
|
||||
d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name])
|
||||
}
|
||||
|
||||
log.debug "created ${d.displayName} with id $dni"
|
||||
d.refresh()
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'"
|
||||
@@ -322,7 +355,7 @@ def addBridge() {
|
||||
def d = getChildDevice(selectedHue)
|
||||
if(!d) {
|
||||
// compatibility with old devices
|
||||
def newbridge = true
|
||||
def newbridge = true
|
||||
childDevices.each {
|
||||
if (it.getDeviceDataByName("mac")) {
|
||||
def newDNI = "${it.getDeviceDataByName("mac")}"
|
||||
@@ -332,22 +365,27 @@ def addBridge() {
|
||||
it.setDeviceNetworkId("${newDNI}")
|
||||
if (oldDNI == selectedHue)
|
||||
app.updateSetting("selectedHue", newDNI)
|
||||
newbridge = false
|
||||
newbridge = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newbridge) {
|
||||
d = addChildDevice("smartthings", "Hue Bridge", selectedHue, vbridge.value.hub)
|
||||
log.debug "created ${d.displayName} with id ${d.deviceNetworkId}"
|
||||
def childDevice = getChildDevice(d.deviceNetworkId)
|
||||
childDevice.sendEvent(name: "serialNumber", value: vbridge.value.serialNumber)
|
||||
if (vbridge.value.ip && vbridge.value.port) {
|
||||
if (vbridge.value.ip.contains("."))
|
||||
if (vbridge.value.ip.contains(".")) {
|
||||
childDevice.sendEvent(name: "networkAddress", value: vbridge.value.ip + ":" + vbridge.value.port)
|
||||
else
|
||||
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
|
||||
} else
|
||||
childDevice.updateDataValue("networkAddress", vbridge.value.ip + ":" + vbridge.value.port)
|
||||
} else {
|
||||
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
|
||||
childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port))
|
||||
}
|
||||
} else {
|
||||
childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
|
||||
childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug "found ${d.displayName} with id $selectedHue already exists"
|
||||
@@ -355,7 +393,6 @@ def addBridge() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def locationHandler(evt) {
|
||||
def description = evt.description
|
||||
log.trace "Location: $description"
|
||||
@@ -397,8 +434,11 @@ def locationHandler(evt) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
networkAddress = d.latestState('networkAddress').stringValue
|
||||
} else {
|
||||
if (d.getDeviceDataByName("networkAddress"))
|
||||
networkAddress = d.getDeviceDataByName("networkAddress")
|
||||
else
|
||||
networkAddress = d.latestState('networkAddress').stringValue
|
||||
log.trace "Host: $host - $networkAddress"
|
||||
if(host != networkAddress) {
|
||||
log.debug "Device's port or ip changed for device $d..."
|
||||
@@ -406,7 +446,8 @@ def locationHandler(evt) {
|
||||
dstate.port = port
|
||||
dstate.name = "Philips hue ($ip)"
|
||||
d.sendEvent(name:"networkAddress", value: host)
|
||||
}
|
||||
d.updateDataValue("networkAddress", host)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -454,17 +495,13 @@ def locationHandler(evt) {
|
||||
|
||||
def doDeviceSync(){
|
||||
log.trace "Doing Hue Device Sync!"
|
||||
|
||||
//shrink the large bulb lists
|
||||
convertBulbListToMap()
|
||||
|
||||
poll()
|
||||
|
||||
if(!state.subscribe) {
|
||||
try {
|
||||
subscribe(location, null, locationHandler, [filterEvents:false])
|
||||
state.subscribe = true
|
||||
}
|
||||
|
||||
} catch (all) {
|
||||
log.trace "Subscription already exist"
|
||||
}
|
||||
discoverBridges()
|
||||
}
|
||||
|
||||
@@ -473,44 +510,49 @@ def doDeviceSync(){
|
||||
/////////////////////////////////////
|
||||
|
||||
def parse(childDevice, description) {
|
||||
def parsedEvent = parseLanMessage(description)
|
||||
def parsedEvent = parseLanMessage(description)
|
||||
if (parsedEvent.headers && parsedEvent.body) {
|
||||
def headerString = parsedEvent.headers.toString()
|
||||
if (headerString?.contains("json")) {
|
||||
def body = new groovy.json.JsonSlurper().parseText(parsedEvent.body)
|
||||
if (body instanceof java.util.HashMap)
|
||||
{ //poll response
|
||||
def bodyString = parsedEvent.body.toString()
|
||||
if (headerString?.contains("json")) {
|
||||
def body
|
||||
try {
|
||||
body = new groovy.json.JsonSlurper().parseText(bodyString)
|
||||
} catch (all) {
|
||||
log.warn "Parsing Body failed - trying again..."
|
||||
poll()
|
||||
}
|
||||
if (body instanceof java.util.HashMap) {
|
||||
//poll response
|
||||
def bulbs = getChildDevices()
|
||||
//for each bulb
|
||||
for (bulb in body) {
|
||||
def d = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"}
|
||||
def d = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"}
|
||||
if (d) {
|
||||
if (bulb.value.state?.reachable) {
|
||||
sendEvent(d.deviceNetworkId, [name: "switch", value: bulb.value?.state?.on ? "on" : "off"])
|
||||
sendEvent(d.deviceNetworkId, [name: "level", value: Math.round(bulb.value.state.bri * 100 / 255)])
|
||||
if (bulb.value.state.sat) {
|
||||
def hue = Math.min(Math.round(bulb.value.state.hue * 100 / 65535), 65535) as int
|
||||
def sat = Math.round(bulb.value.state.sat * 100 / 255) as int
|
||||
def hex = colorUtil.hslToHex(hue, sat)
|
||||
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
|
||||
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
|
||||
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
|
||||
}
|
||||
} else {
|
||||
sendEvent(d.deviceNetworkId, [name: "switch", value: "off"])
|
||||
sendEvent(d.deviceNetworkId, [name: "level", value: 100])
|
||||
if (bulb.value.state.sat) {
|
||||
def hue = 23
|
||||
def sat = 56
|
||||
def hex = colorUtil.hslToHex(23, 56)
|
||||
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
|
||||
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
|
||||
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
|
||||
}
|
||||
}
|
||||
sendEvent(d.deviceNetworkId, [name: "switch", value: bulb.value?.state?.on ? "on" : "off"])
|
||||
sendEvent(d.deviceNetworkId, [name: "level", value: Math.round(bulb.value.state.bri * 100 / 255)])
|
||||
if (bulb.value.state.sat) {
|
||||
def hue = Math.min(Math.round(bulb.value.state.hue * 100 / 65535), 65535) as int
|
||||
def sat = Math.round(bulb.value.state.sat * 100 / 255) as int
|
||||
def hex = colorUtil.hslToHex(hue, sat)
|
||||
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
|
||||
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
|
||||
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
|
||||
}
|
||||
} else {
|
||||
sendEvent(d.deviceNetworkId, [name: "switch", value: "off"])
|
||||
sendEvent(d.deviceNetworkId, [name: "level", value: 100])
|
||||
if (bulb.value.state.sat) {
|
||||
def hue = 23
|
||||
def sat = 56
|
||||
def hex = colorUtil.hslToHex(23, 56)
|
||||
sendEvent(d.deviceNetworkId, [name: "color", value: hex])
|
||||
sendEvent(d.deviceNetworkId, [name: "hue", value: hue])
|
||||
sendEvent(d.deviceNetworkId, [name: "saturation", value: sat])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{ //put response
|
||||
@@ -559,30 +601,29 @@ def parse(childDevice, description) {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug "parse - got something other than headers,body..."
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
def on(childDevice, transition = 4) {
|
||||
def on(childDevice) {
|
||||
log.debug "Executing 'on'"
|
||||
// Assume bulb is off if no current state is found for level to avoid bulbs getting stuck in off after initial discovery
|
||||
def percent = childDevice.device?.currentValue("level") as Integer ?: 0
|
||||
def level = Math.min(Math.round(percent * 255 / 100), 255)
|
||||
put("lights/${getId(childDevice)}/state", [bri: level, on: true, transitiontime: transition])
|
||||
return "level: $percent"
|
||||
put("lights/${getId(childDevice)}/state", [on: true])
|
||||
return "Bulb is On"
|
||||
}
|
||||
|
||||
def off(childDevice, transition = 4) {
|
||||
def off(childDevice) {
|
||||
log.debug "Executing 'off'"
|
||||
put("lights/${getId(childDevice)}/state", [on: false, transitiontime: transition])
|
||||
put("lights/${getId(childDevice)}/state", [on: false])
|
||||
return "Bulb is Off"
|
||||
}
|
||||
|
||||
def setLevel(childDevice, percent) {
|
||||
log.debug "Executing 'setLevel'"
|
||||
def level = Math.min(Math.round(percent * 255 / 100), 255)
|
||||
def level
|
||||
if (percent == 1) level = 1 else level = Math.min(Math.round(percent * 255 / 100), 255)
|
||||
put("lights/${getId(childDevice)}/state", [bri: level, on: percent > 0])
|
||||
}
|
||||
|
||||
@@ -598,19 +639,21 @@ def setHue(childDevice, percent) {
|
||||
put("lights/${getId(childDevice)}/state", [hue: level])
|
||||
}
|
||||
|
||||
def setColor(childDevice, color, alert = "none", transition = 4) {
|
||||
log.debug "Executing 'setColor($color)'"
|
||||
def hue = Math.min(Math.round(color.hue * 65535 / 100), 65535)
|
||||
def sat = Math.min(Math.round(color.saturation * 255 / 100), 255)
|
||||
def setColor(childDevice, huesettings) {
|
||||
log.debug "Executing 'setColor($huesettings)'"
|
||||
def hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535)
|
||||
def sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255)
|
||||
def alert = huesettings.alert ? huesettings.alert : "none"
|
||||
def transition = huesettings.transition ? huesettings.transition : 4
|
||||
|
||||
def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition]
|
||||
if (color.level != null) {
|
||||
value.bri = Math.min(Math.round(color.level * 255 / 100), 255)
|
||||
if (huesettings.level != null) {
|
||||
if (huesettings.level == 1) value.bri = 1 else value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255)
|
||||
value.on = value.bri > 0
|
||||
}
|
||||
|
||||
if (color.switch) {
|
||||
value.on = color.switch == "on"
|
||||
if (huesettings.switch) {
|
||||
value.on = huesettings.switch == "on"
|
||||
}
|
||||
|
||||
log.debug "sending command $value"
|
||||
@@ -640,15 +683,19 @@ private getId(childDevice) {
|
||||
private poll() {
|
||||
def host = getBridgeIP()
|
||||
def uri = "/api/${state.username}/lights/"
|
||||
log.debug "GET: $host$uri"
|
||||
sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1
|
||||
try {
|
||||
sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1
|
||||
HOST: ${host}
|
||||
|
||||
""", physicalgraph.device.Protocol.LAN, selectedHue))
|
||||
} catch (all) {
|
||||
log.warn "Parsing Body failed - trying again..."
|
||||
doDeviceSync()
|
||||
}
|
||||
}
|
||||
|
||||
private put(path, body) {
|
||||
def host = getBridgeIP()
|
||||
def host = getBridgeIP()
|
||||
def uri = "/api/${state.username}/$path"
|
||||
def bodyJSON = new groovy.json.JsonBuilder(body).toString()
|
||||
def length = bodyJSON.getBytes().size().toString()
|
||||
@@ -668,12 +715,19 @@ ${bodyJSON}
|
||||
private getBridgeIP() {
|
||||
def host = null
|
||||
if (selectedHue) {
|
||||
def d = getChildDevice(dni)
|
||||
if (d)
|
||||
host = d.latestState('networkAddress').stringValue
|
||||
def d = getChildDevice(selectedHue)
|
||||
if (d) {
|
||||
if (d.getDeviceDataByName("networkAddress"))
|
||||
host = d.getDeviceDataByName("networkAddress")
|
||||
else
|
||||
host = d.latestState('networkAddress').stringValue
|
||||
}
|
||||
if (host == null || host == "") {
|
||||
def serialNumber = selectedHue
|
||||
def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value
|
||||
if (!bridge) {
|
||||
bridge = getHueBridges().find { it?.value?.mac?.equalsIgnoreCase(serialNumber) }?.value
|
||||
}
|
||||
if (bridge?.ip && bridge?.port) {
|
||||
if (bridge?.ip.contains("."))
|
||||
host = "${bridge?.ip}:${bridge?.port}"
|
||||
@@ -681,9 +735,9 @@ private getBridgeIP() {
|
||||
host = "${convertHexToIP(bridge?.ip)}:${convertHexToInt(bridge?.port)}"
|
||||
} else if (bridge?.networkAddress && bridge?.deviceAddress)
|
||||
host = "${convertHexToIP(bridge?.networkAddress)}:${convertHexToInt(bridge?.deviceAddress)}"
|
||||
}
|
||||
}
|
||||
log.trace "Bridge: $selectedHue - Host: $host"
|
||||
}
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
@@ -720,4 +774,4 @@ private Boolean hasAllHubsOver(String desiredFirmware) {
|
||||
|
||||
private List getRealHubFirmwareVersions() {
|
||||
return location.hubs*.firmwareVersionString.findAll { it }
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,15 @@ def installed() {
|
||||
}
|
||||
|
||||
def updated() {
|
||||
def currentDeviceIds = settings.collect { k, devices -> devices }.flatten().collect { it.id }.unique()
|
||||
def subscriptionDevicesToRemove = app.subscriptions*.device.findAll { device ->
|
||||
!currentDeviceIds.contains(device.id)
|
||||
}
|
||||
subscriptionDevicesToRemove.each { device ->
|
||||
log.debug "Removing $device.displayName subscription"
|
||||
state.remove(device.id)
|
||||
unsubscribe(device)
|
||||
}
|
||||
log.debug settings
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ definition(
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/life360.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/life360@2x.png",
|
||||
oauth: [displayName: "Life360", displayLink: "Life360"]
|
||||
oauth: [displayName: "Life360", displayLink: "Life360"],
|
||||
singleInstance: true
|
||||
) {
|
||||
appSetting "clientId"
|
||||
appSetting "clientSecret"
|
||||
@@ -591,7 +592,7 @@ def updated() {
|
||||
// log.debug "External Id=${app.id}:${member.id}"
|
||||
|
||||
// create the device
|
||||
def childDevice = addChildDevice("smartthings", "life360-user", "${app.id}.${member.id}",null,[name:member.firstName, completedSetup: true])
|
||||
def childDevice = addChildDevice("smartthings", "Life360 User", "${app.id}.${member.id}",null,[name:member.firstName, completedSetup: true])
|
||||
// childDevice.setMemberId(member.id)
|
||||
|
||||
if (childDevice)
|
||||
|
||||
404
smartapps/smartthings/lifx-connect.src/lifx-connect.groovy
Normal file
404
smartapps/smartthings/lifx-connect.src/lifx-connect.groovy
Normal file
@@ -0,0 +1,404 @@
|
||||
/**
|
||||
* LIFX
|
||||
*
|
||||
* Copyright 2015 LIFX
|
||||
*
|
||||
*/
|
||||
definition(
|
||||
name: "LIFX (Connect)",
|
||||
namespace: "smartthings",
|
||||
author: "LIFX",
|
||||
description: "Allows you to use LIFX smart light bulbs with SmartThings.",
|
||||
category: "Convenience",
|
||||
iconUrl: "https://cloud.lifx.com/images/lifx.png",
|
||||
iconX2Url: "https://cloud.lifx.com/images/lifx.png",
|
||||
iconX3Url: "https://cloud.lifx.com/images/lifx.png",
|
||||
oauth: true,
|
||||
singleInstance: true) {
|
||||
appSetting "clientId"
|
||||
appSetting "clientSecret"
|
||||
}
|
||||
|
||||
|
||||
preferences {
|
||||
page(name: "Credentials", title: "LIFX", content: "authPage", install: true)
|
||||
}
|
||||
|
||||
mappings {
|
||||
path("/receivedToken") { action: [ POST: "oauthReceivedToken", GET: "oauthReceivedToken"] }
|
||||
path("/receiveToken") { action: [ POST: "oauthReceiveToken", GET: "oauthReceiveToken"] }
|
||||
path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] }
|
||||
path("/oauth/callback") { action: [ GET: "oauthCallback" ] }
|
||||
path("/oauth/initialize") { action: [ GET: "oauthInit"] }
|
||||
path("/test") { action: [ GET: "oauthSuccess" ] }
|
||||
}
|
||||
|
||||
def getServerUrl() { return "https://graph.api.smartthings.com" }
|
||||
def getCallbackUrl() { return "https://graph.api.smartthings.com/oauth/callback"}
|
||||
def apiURL(path = '/') { return "https://api.lifx.com/v1${path}" }
|
||||
def getSecretKey() { return appSettings.secretKey }
|
||||
def getClientId() { return appSettings.clientId }
|
||||
|
||||
def authPage() {
|
||||
log.debug "authPage test1"
|
||||
if (!state.lifxAccessToken) {
|
||||
log.debug "no LIFX access token"
|
||||
// This is the SmartThings access token
|
||||
if (!state.accessToken) {
|
||||
log.debug "no access token, create access token"
|
||||
state.accessToken = createAccessToken() // predefined method
|
||||
}
|
||||
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}"
|
||||
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")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug "have LIFX access token"
|
||||
|
||||
def options = locationOptions() ?: []
|
||||
def count = options.size()
|
||||
|
||||
return dynamicPage(name:"Credentials", title:"", nextPage:"", install:true, uninstall: true) {
|
||||
section("Select your location") {
|
||||
input "selectedLocationId", "enum", required:true, title:"Select location (${count} found)", multiple:false, options:options, submitOnChange: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OAuth
|
||||
|
||||
def oauthInit() {
|
||||
def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote_control:all", response_type: "code" ]
|
||||
log.info("Redirecting user to OAuth setup")
|
||||
redirect(location: "https://cloud.lifx.com/oauth/authorize?${toQueryString(oauthParams)}")
|
||||
}
|
||||
|
||||
def oauthCallback() {
|
||||
def redirectUrl = null
|
||||
if (params.authQueryString) {
|
||||
redirectUrl = URLDecoder.decode(params.authQueryString.replaceAll(".+&redirect_url=", ""))
|
||||
} else {
|
||||
log.warn "No authQueryString"
|
||||
}
|
||||
|
||||
if (state.lifxAccessToken) {
|
||||
log.debug "Access token already exists"
|
||||
success()
|
||||
} else {
|
||||
def code = params.code
|
||||
if (code) {
|
||||
if (code.size() > 6) {
|
||||
// LIFX code
|
||||
log.debug "Exchanging code for access token"
|
||||
oauthReceiveToken(redirectUrl)
|
||||
} else {
|
||||
// Initiate the LIFX OAuth flow.
|
||||
oauthInit()
|
||||
}
|
||||
} else {
|
||||
log.debug "This code should be unreachable"
|
||||
success()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def oauthReceiveToken(redirectUrl = null) {
|
||||
// Not sure what redirectUrl is for
|
||||
log.debug "receiveToken - params: ${params}"
|
||||
def oauthParams = [ client_id: "${appSettings.clientId}", client_secret: "${appSettings.clientSecret}", grant_type: "authorization_code", code: params.code, scope: params.scope ] // how is params.code valid here?
|
||||
def params = [
|
||||
uri: "https://cloud.lifx.com/oauth/token",
|
||||
body: oauthParams,
|
||||
headers: [
|
||||
"User-Agent": "SmartThings Integration"
|
||||
]
|
||||
]
|
||||
httpPost(params) { response ->
|
||||
state.lifxAccessToken = response.data.access_token
|
||||
}
|
||||
|
||||
if (state.lifxAccessToken) {
|
||||
oauthSuccess()
|
||||
} else {
|
||||
oauthFailure()
|
||||
}
|
||||
}
|
||||
|
||||
def oauthSuccess() {
|
||||
def message = """
|
||||
<p>Your LIFX Account is now connected to SmartThings!</p>
|
||||
<p>Click 'Done' to finish setup.</p>
|
||||
"""
|
||||
oauthConnectionStatus(message)
|
||||
}
|
||||
|
||||
def oauthFailure() {
|
||||
def message = """
|
||||
<p>The connection could not be established!</p>
|
||||
<p>Click 'Done' to return to the menu.</p>
|
||||
"""
|
||||
oauthConnectionStatus(message)
|
||||
}
|
||||
|
||||
def oauthReceivedToken() {
|
||||
def message = """
|
||||
<p>Your LIFX Account is already connected to SmartThings!</p>
|
||||
<p>Click 'Done' to finish setup.</p>
|
||||
"""
|
||||
oauthConnectionStatus(message)
|
||||
}
|
||||
|
||||
def oauthConnectionStatus(message, redirectUrl = null) {
|
||||
def redirectHtml = ""
|
||||
if (redirectUrl) {
|
||||
redirectHtml = """
|
||||
<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
|
||||
"""
|
||||
}
|
||||
|
||||
def html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>SmartThings Connection</title>
|
||||
<style type="text/css">
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-thin-webfont.svg#swis721_th_btthin') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot');
|
||||
src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot?#iefix') format('embedded-opentype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.woff') format('woff'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.ttf') format('truetype'),
|
||||
url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.svg#swis721_lt_btlight') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
.container {
|
||||
width: 280;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
img:nth-child(2) {
|
||||
margin: 0 15px;
|
||||
}
|
||||
p {
|
||||
font-size: 1.2em;
|
||||
font-family: 'Swiss 721 W01 Thin';
|
||||
text-align: center;
|
||||
color: #666666;
|
||||
padding: 0 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
span {
|
||||
font-family: 'Swiss 721 W01 Light';
|
||||
}
|
||||
</style>
|
||||
${redirectHtml}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src='https://cloud.lifx.com/images/lifx.png' alt='LIFX icon' width='100'/>
|
||||
<img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/connected-device-icn%402x.png' alt='connected device icon' width="40"/>
|
||||
<img src='https://s3.amazonaws.com/smartapp-icons/Partner/support/st-logo%402x.png' alt='SmartThings logo' width="100"/>
|
||||
<p>
|
||||
${message}
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
render contentType: 'text/html', data: html
|
||||
}
|
||||
|
||||
String toQueryString(Map m) {
|
||||
return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&")
|
||||
}
|
||||
|
||||
// App lifecycle hooks
|
||||
|
||||
def installed() {
|
||||
if (!state.accessToken) {
|
||||
createAccessToken()
|
||||
} else {
|
||||
initialize()
|
||||
}
|
||||
// Check for new devices and remove old ones every 3 hours
|
||||
runEvery3Hours('updateDevices')
|
||||
}
|
||||
|
||||
// called after settings are changed
|
||||
def updated() {
|
||||
if (!state.accessToken) {
|
||||
createAccessToken()
|
||||
} else {
|
||||
initialize()
|
||||
}
|
||||
}
|
||||
|
||||
def uninstalled() {
|
||||
log.info("Uninstalling, removing child devices...")
|
||||
unschedule('updateDevices')
|
||||
removeChildDevices(getChildDevices())
|
||||
}
|
||||
|
||||
private removeChildDevices(devices) {
|
||||
devices.each {
|
||||
deleteChildDevice(it.deviceNetworkId) // 'it' is default
|
||||
}
|
||||
}
|
||||
|
||||
// called after Done is hit after selecting a Location
|
||||
def initialize() {
|
||||
log.debug "initialize"
|
||||
updateDevices()
|
||||
}
|
||||
|
||||
// Misc
|
||||
|
||||
Map apiRequestHeaders() {
|
||||
return ["Authorization": "Bearer ${state.lifxAccessToken}",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "SmartThings Integration"
|
||||
]
|
||||
}
|
||||
|
||||
// Requests
|
||||
|
||||
def logResponse(response) {
|
||||
log.info("Status: ${response.status}")
|
||||
log.info("Body: ${response.data}")
|
||||
}
|
||||
|
||||
// API Requests
|
||||
// logObject is because log doesn't work if this method is being called from a Device
|
||||
def logErrors(options = [errorReturn: null, logObject: log], Closure c) {
|
||||
try {
|
||||
return c()
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
options.logObject.error("got error: ${e}, body: ${e.getResponse().getData()}")
|
||||
if (e.statusCode == 401) { // token is expired
|
||||
state.remove("lifxAccessToken")
|
||||
options.logObject.warn "Access token is not valid"
|
||||
}
|
||||
return options.errorReturn
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
options.logObject.warn "Connection timed out, not much we can do here"
|
||||
return options.errorReturn
|
||||
}
|
||||
}
|
||||
|
||||
def apiGET(path) {
|
||||
try {
|
||||
httpGet(uri: apiURL(path), headers: apiRequestHeaders()) {response ->
|
||||
logResponse(response)
|
||||
return response
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
logResponse(e.response)
|
||||
return e.response
|
||||
}
|
||||
}
|
||||
|
||||
def apiPUT(path, body = [:]) {
|
||||
try {
|
||||
log.debug("Beginning API PUT: ${path}, ${body}")
|
||||
httpPutJson(uri: apiURL(path), body: new groovy.json.JsonBuilder(body).toString(), headers: apiRequestHeaders(), ) {response ->
|
||||
logResponse(response)
|
||||
return response
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
logResponse(e.response)
|
||||
return e.response
|
||||
}}
|
||||
|
||||
def devicesList(selector = '') {
|
||||
logErrors([]) {
|
||||
def resp = apiGET("/lights/${selector}")
|
||||
if (resp.status == 200) {
|
||||
return resp.data
|
||||
} else {
|
||||
log.error("Non-200 from device list call. ${resp.status} ${resp.data}")
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map locationOptions() {
|
||||
def options = [:]
|
||||
def devices = devicesList()
|
||||
devices.each { device ->
|
||||
options[device.location.id] = device.location.name
|
||||
}
|
||||
log.debug("Locations: ${options}")
|
||||
return options
|
||||
}
|
||||
|
||||
def devicesInLocation() {
|
||||
return devicesList("location_id:${settings.selectedLocationId}")
|
||||
}
|
||||
|
||||
// ensures the devices list is up to date
|
||||
def updateDevices() {
|
||||
if (!state.devices) {
|
||||
state.devices = [:]
|
||||
}
|
||||
def devices = devicesInLocation()
|
||||
def selectors = []
|
||||
|
||||
log.debug("All selectors: ${selectors}")
|
||||
|
||||
devices.each { device ->
|
||||
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
|
||||
]
|
||||
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)
|
||||
} else {
|
||||
childDevice = addChildDevice(app.namespace, "LIFX White Bulb", device.id, null, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
getChildDevices().findAll { !selectors.contains("${it.deviceNetworkId}") }.each {
|
||||
log.info("Deleting ${it.deviceNetworkId}")
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -34,20 +34,20 @@
|
||||
* locks | lock | lock, unlock | locked, unlocked
|
||||
* ---------------------+-------------------+-----------------------------+------------------------------------
|
||||
*/
|
||||
|
||||
|
||||
definition(
|
||||
name: "Logitech Harmony (Connect)",
|
||||
namespace: "smartthings",
|
||||
author: "Juan Pablo Risso",
|
||||
author: "SmartThings",
|
||||
description: "Allows you to integrate your Logitech Harmony account with SmartThings.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony%402x.png",
|
||||
oauth: [displayName: "Logitech Harmony", displayLink: "http://www.logitech.com/en-us/harmony-remotes"]
|
||||
oauth: [displayName: "Logitech Harmony", displayLink: "http://www.logitech.com/en-us/harmony-remotes"],
|
||||
singleInstance: true
|
||||
){
|
||||
appSetting "clientId"
|
||||
appSetting "clientSecret"
|
||||
appSetting "callbackUrl"
|
||||
}
|
||||
|
||||
preferences(oauthPage: "deviceAuthorization") {
|
||||
@@ -89,16 +89,18 @@ mappings {
|
||||
}
|
||||
|
||||
def getServerUrl() { return "https://graph.api.smartthings.com" }
|
||||
def getCallbackUrl() { "https://graph.api.smartthings.com/oauth/callback" }
|
||||
def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" }
|
||||
|
||||
def authPage() {
|
||||
def description = null
|
||||
def description = null
|
||||
if (!state.HarmonyAccessToken) {
|
||||
if (!state.accessToken) {
|
||||
log.debug "About to create access token"
|
||||
createAccessToken()
|
||||
}
|
||||
description = "Click to enter Harmony Credentials"
|
||||
def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}"
|
||||
def redirectUrl = buildRedirectUrl
|
||||
return dynamicPage(name: "Credentials", title: "Harmony", nextPage: null, uninstall: true, install:false) {
|
||||
section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description }
|
||||
}
|
||||
@@ -110,7 +112,7 @@ def authPage() {
|
||||
|
||||
def huboptions = state.HarmonyHubs ?: []
|
||||
def actoptions = state.HarmonyActivities ?: []
|
||||
|
||||
|
||||
def numFoundHub = huboptions.size() ?: 0
|
||||
def numFoundAct = actoptions.size() ?: 0
|
||||
if((deviceRefreshCount % 5) == 0) {
|
||||
@@ -120,13 +122,14 @@ def authPage() {
|
||||
section("Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") {
|
||||
input "selectedhubs", "enum", required:false, title:"Select Harmony Hubs (${numFoundHub} found)", multiple:true, options:huboptions
|
||||
}
|
||||
if (numFoundHub > 0 && numFoundAct > 0 && false)
|
||||
// Virtual activity flag
|
||||
if (numFoundHub > 0 && numFoundAct > 0 && true)
|
||||
section("You can also add activities as virtual switches for other convenient integrations") {
|
||||
input "selectedactivities", "enum", required:false, title:"Select Harmony Activities (${numFoundAct} found)", multiple:true, options:actoptions
|
||||
}
|
||||
}
|
||||
if (state.resethub)
|
||||
section("Connection to the hub timed out. Please restart the hub and try again.") {}
|
||||
}
|
||||
section("Connection to the hub timed out. Please restart the hub and try again.") {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +141,7 @@ def callback() {
|
||||
} else {
|
||||
log.warn "No authQueryString"
|
||||
}
|
||||
|
||||
|
||||
if (state.HarmonyAccessToken) {
|
||||
log.debug "Access token already exists"
|
||||
discovery()
|
||||
@@ -163,8 +166,8 @@ def callback() {
|
||||
|
||||
def init() {
|
||||
log.debug "Requesting Code"
|
||||
def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${appSettings.callbackUrl}" ]
|
||||
redirect(location: "https://home.myharmony.com/oauth2/authorize?${toQueryString(oauthParams)}")
|
||||
def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${callbackUrl}" ]
|
||||
redirect(location: "https://home.myharmony.com/oauth2/authorize?${toQueryString(oauthParams)}")
|
||||
}
|
||||
|
||||
def receiveToken(redirectUrl = null) {
|
||||
@@ -174,7 +177,7 @@ def receiveToken(redirectUrl = null) {
|
||||
uri: "https://home.myharmony.com/oauth2/token?${toQueryString(oauthParams)}",
|
||||
]
|
||||
try {
|
||||
httpPost(params) { response ->
|
||||
httpPost(params) { response ->
|
||||
state.HarmonyAccessToken = response.data.access_token
|
||||
}
|
||||
} catch (java.util.concurrent.TimeoutException e) {
|
||||
@@ -221,7 +224,7 @@ def connectionStatus(message, redirectUrl = null) {
|
||||
<meta http-equiv="refresh" content="3; url=${redirectUrl}" />
|
||||
"""
|
||||
}
|
||||
|
||||
|
||||
def html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -302,30 +305,28 @@ def buildRedirectUrl(page) {
|
||||
}
|
||||
|
||||
def installed() {
|
||||
enableCallback()
|
||||
if (!state.accessToken) {
|
||||
log.debug "About to create access token"
|
||||
createAccessToken()
|
||||
} else {
|
||||
} else {
|
||||
initialize()
|
||||
}
|
||||
}
|
||||
|
||||
def updated() {
|
||||
unsubscribe()
|
||||
unschedule()
|
||||
enableCallback()
|
||||
unschedule()
|
||||
if (!state.accessToken) {
|
||||
log.debug "About to create access token"
|
||||
createAccessToken()
|
||||
} else {
|
||||
} else {
|
||||
initialize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def uninstalled() {
|
||||
if (state.HarmonyAccessToken) {
|
||||
try {
|
||||
try {
|
||||
state.HarmonyAccessToken = ""
|
||||
log.debug "Success disconnecting Harmony from SmartThings"
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
@@ -339,7 +340,7 @@ def initialize() {
|
||||
if (selectedhubs || selectedactivities) {
|
||||
addDevice()
|
||||
runEvery5Minutes("discovery")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getHarmonydevices() {
|
||||
@@ -359,20 +360,20 @@ Map discoverDevices() {
|
||||
def hubname = getHubName(it.key)
|
||||
def hubvalue = "${hubname}"
|
||||
hubs["harmony-${hubkey}"] = hubvalue
|
||||
it.value.response.data.activities.each {
|
||||
it.value.response.data.activities.each {
|
||||
def value = "${it.value.name}"
|
||||
def key = "harmony-${hubkey}-${it.key}"
|
||||
activities["${key}"] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
state.HarmonyHubs = hubs
|
||||
state.HarmonyActivities = activities
|
||||
}
|
||||
state.HarmonyActivities = activities
|
||||
}
|
||||
}
|
||||
|
||||
//CHILD DEVICE METHODS
|
||||
def discovery() {
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}"
|
||||
try {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
@@ -384,22 +385,24 @@ def discovery() {
|
||||
poll()
|
||||
} else {
|
||||
log.debug "Error: $response.status"
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
if (e.statusCode == 401) { // token is expired
|
||||
state.remove("HarmonyAccessToken")
|
||||
state.remove("HarmonyAccessToken")
|
||||
log.warn "Harmony Access token has expired"
|
||||
}
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
log.warn "Connection to the hub timed out. Please restart the hub and try again."
|
||||
state.resethub = true
|
||||
}
|
||||
} catch (e) {
|
||||
log.warn "Hostname in certificate didn't match. Please try again later."
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
def addDevice() {
|
||||
log.trace "Adding Hubs"
|
||||
log.trace "Adding Hubs"
|
||||
selectedhubs.each { dni ->
|
||||
def d = getChildDevice(dni)
|
||||
if(!d) {
|
||||
@@ -410,8 +413,8 @@ def addDevice() {
|
||||
} else {
|
||||
log.trace "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
log.trace "Adding Activities"
|
||||
}
|
||||
log.trace "Adding Activities"
|
||||
selectedactivities.each { dni ->
|
||||
def d = getChildDevice(dni)
|
||||
if(!d) {
|
||||
@@ -422,7 +425,7 @@ def addDevice() {
|
||||
} else {
|
||||
log.trace "found ${d.displayName} with id $dni already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def activity(dni,mode) {
|
||||
@@ -430,26 +433,26 @@ def activity(dni,mode) {
|
||||
def msg = "Command failed"
|
||||
def url = ''
|
||||
if (dni == "all") {
|
||||
url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(Params)}"
|
||||
} else {
|
||||
url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(Params)}"
|
||||
} else {
|
||||
def aux = dni.split('-')
|
||||
def hubId = aux[1]
|
||||
if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){
|
||||
url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(Params)}"
|
||||
if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){
|
||||
url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(Params)}"
|
||||
} else {
|
||||
def activityId = aux[2]
|
||||
url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/${activityId}/${mode}?${toQueryString(Params)}"
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
httpPostJson(uri: url) { response ->
|
||||
httpPostJson(uri: url) { response ->
|
||||
if (response.data.code == 200 || dni == "all") {
|
||||
msg = "Command sent succesfully"
|
||||
state.aux = 0
|
||||
state.aux = 0
|
||||
} else {
|
||||
msg = "Command failed. Error: $response.data.code"
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException ex) {
|
||||
log.error ex
|
||||
if (state.aux == 0) {
|
||||
@@ -457,9 +460,11 @@ def activity(dni,mode) {
|
||||
activity(dni,mode)
|
||||
} else {
|
||||
msg = ex
|
||||
state.aux = 0
|
||||
state.aux = 0
|
||||
}
|
||||
}
|
||||
} catch(Exception ex) {
|
||||
msg = ex
|
||||
}
|
||||
runIn(10, "poll", [overwrite: true])
|
||||
return msg
|
||||
}
|
||||
@@ -468,10 +473,10 @@ def poll() {
|
||||
// GET THE LIST OF ACTIVITIES
|
||||
if (state.HarmonyAccessToken) {
|
||||
getActivityList()
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def url = "https://home.myharmony.com/cloudapi/state?${toQueryString(Params)}"
|
||||
try {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
def map = [:]
|
||||
response.data.hubs.each {
|
||||
if (it.value.message == "OK") {
|
||||
@@ -484,20 +489,20 @@ def poll() {
|
||||
def currentActivity = getActivityName(it.value.response.data.currentAvActivity,it.key)
|
||||
hub.sendEvent(name: "currentActivity", value: currentActivity, descriptionText: "Current activity is ${currentActivity}", display: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.trace it.value.message
|
||||
}
|
||||
}
|
||||
}
|
||||
def activities = getChildDevices()
|
||||
def activities = getChildDevices()
|
||||
def activitynotrunning = true
|
||||
activities.each { activity ->
|
||||
def act = activity.deviceNetworkId.split('-')
|
||||
def act = activity.deviceNetworkId.split('-')
|
||||
if (act.size() > 2) {
|
||||
def aux = map.find { it.key == act[1] }
|
||||
if (aux) {
|
||||
def aux2 = aux.value.split(',')
|
||||
def childDevice = getChildDevice(activity.deviceNetworkId)
|
||||
def childDevice = getChildDevice(activity.deviceNetworkId)
|
||||
if ((act[2] == aux2[0]) && (aux2[1] == "1" || aux2[1] == "2")) {
|
||||
childDevice?.sendEvent(name: "switch", value: "on")
|
||||
if (aux2[1] == "1")
|
||||
@@ -507,28 +512,30 @@ def poll() {
|
||||
if (aux2[1] == "3")
|
||||
runIn(5, "poll", [overwrite: true])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "Poll completed $map - $state.hubs"
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
if (e.statusCode == 401) { // token is expired
|
||||
state.remove("HarmonyAccessToken")
|
||||
state.remove("HarmonyAccessToken")
|
||||
return "Harmony Access token has expired"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def getActivityList() {
|
||||
// GET ACTIVITY'S NAME
|
||||
if (state.HarmonyAccessToken) {
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}"
|
||||
try {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
response.data.hubs.each {
|
||||
def hub = getChildDevice("harmony-${it.key}")
|
||||
if (hub) {
|
||||
@@ -541,17 +548,19 @@ def getActivityList() {
|
||||
}
|
||||
activities += [id: "off", name: "Activity OFF", type: "0"]
|
||||
log.trace activities
|
||||
}
|
||||
hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", display: false)
|
||||
}
|
||||
hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", display: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
log.trace e
|
||||
} catch (java.net.SocketTimeoutException e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
return activity
|
||||
}
|
||||
|
||||
@@ -559,16 +568,16 @@ def getActivityName(activity,hubId) {
|
||||
// GET ACTIVITY'S NAME
|
||||
def actname = activity
|
||||
if (state.HarmonyAccessToken) {
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}"
|
||||
try {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
actname = response.data.data.activities[activity].name
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return actname
|
||||
}
|
||||
|
||||
@@ -576,19 +585,19 @@ def getActivityId(activity,hubId) {
|
||||
// GET ACTIVITY'S NAME
|
||||
def actid = activity
|
||||
if (state.HarmonyAccessToken) {
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}"
|
||||
try {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
response.data.data.activities.each {
|
||||
if (it.value.name == activity)
|
||||
actid = it.key
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return actid
|
||||
}
|
||||
|
||||
@@ -596,16 +605,16 @@ def getHubName(hubId) {
|
||||
// GET HUB'S NAME
|
||||
def hubname = hubId
|
||||
if (state.HarmonyAccessToken) {
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def Params = [auth: state.HarmonyAccessToken]
|
||||
def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/discover?${toQueryString(Params)}"
|
||||
try {
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
httpGet(uri: url, headers: ["Accept": "application/json"]) {response ->
|
||||
hubname = response.data.data.name
|
||||
}
|
||||
} catch (groovyx.net.http.HttpResponseException e) {
|
||||
} catch(Exception e) {
|
||||
log.trace e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return hubname
|
||||
}
|
||||
|
||||
@@ -616,8 +625,8 @@ def sendNotification(msg) {
|
||||
def hookEventHandler() {
|
||||
// log.debug "In hookEventHandler method."
|
||||
log.debug "request = ${request}"
|
||||
|
||||
def json = request.JSON
|
||||
|
||||
def json = request.JSON
|
||||
|
||||
def html = """{"code":200,"message":"OK"}"""
|
||||
render contentType: 'application/json', data: html
|
||||
@@ -651,12 +660,16 @@ def updateDevice() {
|
||||
} else {
|
||||
def device = allDevices.find { it.id == params.id }
|
||||
if (device) {
|
||||
if (arguments) {
|
||||
device."$command"(*arguments)
|
||||
} else {
|
||||
device."$command"()
|
||||
if (device.hasCommand("$command")) {
|
||||
if (arguments) {
|
||||
device."$command"(*arguments)
|
||||
} else {
|
||||
device."$command"()
|
||||
}
|
||||
render status: 204, data: "{}"
|
||||
} else {
|
||||
render status: 404, data: '{"msg": "Command not supported by this Device"}'
|
||||
}
|
||||
render status: 204, data: "{}"
|
||||
} else {
|
||||
render status: 404, data: '{"msg": "Device not found"}'
|
||||
}
|
||||
|
||||
@@ -79,12 +79,9 @@ def scenePage(params=[:]) {
|
||||
href "devicePage", title: "Show Device States", params: [sceneId:sceneId], description: "", state: sceneIsDefined(sceneId) ? "complete" : "incomplete"
|
||||
}
|
||||
|
||||
if (sceneId == currentSceneId) {
|
||||
section {
|
||||
href "saveStatesPage", title: "Record Current Device States", params: [sceneId:sceneId], description: ""
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
href "saveStatesPage", title: "Record Current Device States", params: [sceneId:sceneId], description: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +222,7 @@ private restoreStates(sceneId) {
|
||||
if (type == "level") {
|
||||
log.debug "${light.displayName} level is '$level'"
|
||||
if (level != null) {
|
||||
light.setLevel(value)
|
||||
light.setLevel(level)
|
||||
}
|
||||
}
|
||||
else if (type == "color") {
|
||||
|
||||
@@ -49,6 +49,7 @@ preferences {
|
||||
section("Via a push notification and/or an SMS message"){
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false
|
||||
paragraph "If outside the US please make sure to enter the proper country code"
|
||||
input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes", "No"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ definition(
|
||||
description: "Allows you to control your Samsung TV from the SmartThings app. Perform basic functions like power Off, source, volume, channels and other remote control functions.",
|
||||
category: "SmartThings Labs",
|
||||
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%402x.png",
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%403x.png"
|
||||
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%403x.png",
|
||||
singleInstance: true
|
||||
)
|
||||
|
||||
preferences {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user