Changing remote branch to PLRG Git server.
[smartapps.git] / third-party / QuirkyTripper.groovy
1 /**
2  *  Quirky/Wink Tripper Contact Sensor
3  *
4  *  Copyright 2015 Mitch Pond, SmartThings
5  *
6  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7  *  in compliance with the License. You may obtain a copy of the License at:
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12  *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13  *  for the specific language governing permissions and limitations under the License.
14  *
15  */
16
17 metadata {
18         definition (name: "Quirky/Wink Tripper", namespace: "mitchpond", author: "Mitch Pond") {
19     
20                 capability "Contact Sensor"
21                 capability "Battery"
22                 capability "Configuration"
23                 capability "Sensor"
24     
25                 attribute "tamper", "string"
26     
27                 command "configure"
28                 command "resetTamper"
29         
30                 fingerprint endpointId: "01", profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,0500,0020,0B05", outClusters: "0003,0019"
31         }
32
33         // simulator metadata
34         simulator {}
35
36         // UI tile definitions
37         tiles {
38                 standardTile("contact", "device.contact", width: 2, height: 2, canChangeIcon: true) {
39                         state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
40                         state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821")
41                 }
42         
43                 valueTile("battery", "device.battery", decoration: "flat") {
44                         state "battery", label:'${currentValue}% battery', unit:""
45                 }
46         
47                 standardTile("tamper", "device.tamper") {
48                         state "OK", label: "Tamper OK", icon: "st.security.alarm.on", backgroundColor:"#79b821", decoration: "flat"
49                         state "tampered", label: "Tampered", action: "resetTamper", icon: "st.security.alarm.off", backgroundColor:"#ffa81e", decoration: "flat"
50                 }
51         
52                 main ("contact")
53                 details(["contact","battery","tamper"])
54         }
55 }
56
57 // Parse incoming device messages to generate events
58 def parse(String description) {
59         //log.debug "description: $description"
60
61         def results = []
62         if (description?.startsWith('catchall:')) {
63                 results = parseCatchAllMessage(description)
64         }
65         else if (description?.startsWith('read attr -')) {
66                 results = parseReportAttributeMessage(description)
67         }
68         else if (description?.startsWith('zone status')) {
69                 results = parseIasMessage(description)
70         }
71
72         log.debug "Parse returned $results"
73
74         if (description?.startsWith('enroll request')) {
75                 List cmds = enrollResponse()
76                 log.debug "enroll response: ${cmds}"
77                 results = cmds?.collect { new physicalgraph.device.HubAction(it) }
78         }
79         return results
80 }
81
82 //Initializes device and sets up reporting
83 def configure() {
84         String zigbeeId = swapEndianHex(device.hub.zigbeeId)
85         log.debug "Confuguring Reporting, IAS CIE, and Bindings."
86     
87         def cmd = [
88                 "zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200",
89                 "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
90         
91                 "zcl global send-me-a-report 0x500 0x0012 0x19 0 0xFF {}", "delay 200", //get notified on tamper
92                 "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
93                 
94                 "zcl global send-me-a-report 1 0x20 0x20 5 3600 {}", "delay 200", //battery report request
95                 "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
96         
97                 "zdo bind 0x${device.deviceNetworkId} 1 1 0x500 {${device.zigbeeId}} {}", "delay 500",
98                 "zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500",
99                 "st rattr 0x${device.deviceNetworkId} 1 1 0x20"
100                 ]
101         cmd
102 }
103
104 //Sends IAS Zone Enroll response
105 def enrollResponse() {
106         log.debug "Sending enroll response"
107         [       
108         "raw 0x500 {01 23 00 00 00}", "delay 200",
109         "send 0x${device.deviceNetworkId} 1 1"
110         ]
111 }
112
113 private Map parseCatchAllMessage(String description) {
114         def results = [:]
115         def cluster = zigbee.parse(description)
116         if (shouldProcessMessage(cluster)) {
117                 switch(cluster.clusterId) {
118                         case 0x0001:
119                                 log.debug "Received a catchall message for battery status. This should not happen."
120                                 results << createEvent(getBatteryResult(cluster.data.last()))
121                                 break
122             }
123         }
124
125         return results
126 }
127
128 private boolean shouldProcessMessage(cluster) {
129         // 0x0B is default response indicating message got through
130         // 0x07 is bind message
131         boolean ignoredMessage = cluster.profileId != 0x0104 || 
132                 cluster.command == 0x0B ||
133                 cluster.command == 0x07 ||
134                 (cluster.data.size() > 0 && cluster.data.first() == 0x3e)
135         return !ignoredMessage
136 }
137
138 private parseReportAttributeMessage(String description) {
139         Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
140                 def nameAndValue = param.split(":")
141                 map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
142         }
143         //log.debug "Desc Map: $descMap"
144
145         def results = []
146     
147         if (descMap.cluster == "0001" && descMap.attrId == "0020") {
148                 log.debug "Received battery level report"
149                 results = createEvent(getBatteryResult(Integer.parseInt(descMap.value, 16)))
150         }
151
152         return results
153 }
154
155 private parseIasMessage(String description) {
156         List parsedMsg = description.split(' ')
157         String msgCode = parsedMsg[2]
158         int status = Integer.decode(msgCode)
159         def linkText = getLinkText(device)
160
161         def results = []
162         //log.debug(description)
163         if (status & 0b00000001) {results << createEvent(getContactResult('open'))}
164         else if (~status & 0b00000001) results << createEvent(getContactResult('closed'))
165
166         if (status & 0b00000100) {
167                 //log.debug "Tampered"
168             results << createEvent([name: "tamper", value:"tampered"])
169         }
170         else if (~status & 0b00000100) {
171                 //don't reset the status here as we want to force a manual reset
172                 //log.debug "Not tampered"
173                 //results << createEvent([name: "tamper", value:"OK"])
174         }
175         
176         if (status & 0b00001000) {
177                 //battery reporting seems unreliable with these devices. However, they do report when low.
178                 //Just in case the battery level reporting has stopped working, we'll at least catch the low battery warning.
179                 //
180                 //** Commented this out as this is currently conflicting with the battery level report **/
181                 //log.debug "${linkText} reports low battery!"
182                 //results << createEvent([name: "battery", value: 10])
183         }
184         else if (~status & 0b00001000) {
185                 //log.debug "${linkText} battery OK"
186         }
187         //log.debug results
188         return results
189 }
190
191 //Converts the battery level response into a percentage to display in ST
192 //and creates appropriate message for given level
193 //**real-world testing with this device shows that 2.4v is about as low as it can go **/
194
195 private getBatteryResult(rawValue) {
196         def linkText = getLinkText(device)
197
198         def result = [name: 'battery']
199
200         def volts = rawValue / 10
201         def descriptionText
202         if (volts > 3.5) {
203                 result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
204         }
205         else {
206                 def minVolts = 2.4
207                 def maxVolts = 3.0
208                 def pct = (volts - minVolts) / (maxVolts - minVolts)
209                 result.value = Math.min(100, (int) pct * 100)
210                 result.descriptionText = "${linkText} battery was ${result.value}%"
211         }
212
213         return result
214 }
215
216
217 private Map getContactResult(value) {
218         def linkText = getLinkText(device)
219         def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}"
220         return [
221                 name: 'contact',
222                 value: value,
223                 descriptionText: descriptionText
224                 ]
225 }
226
227 //Resets the tamper switch state
228 private resetTamper(){
229         log.debug "Tamper alarm reset."
230         sendEvent([name: "tamper", value:"OK"])
231 }
232
233 private hex(value) {
234         new BigInteger(Math.round(value).toString()).toString(16)
235 }
236
237 private String swapEndianHex(String hex) {
238         reverseArray(hex.decodeHex()).encodeHex()
239 }
240
241 private byte[] reverseArray(byte[] array) {
242         int i = 0;
243         int j = array.length - 1;
244         byte tmp;
245         while (j > i) {
246                 tmp = array[j];
247                 array[j] = array[i];
248                 array[i] = tmp;
249                 j--;
250                 i++;
251         }
252         return array
253 }