2 * Quirky/Wink Tripper Contact Sensor
4 * Copyright 2015 Mitch Pond, SmartThings
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:
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
18 definition (name: "Quirky/Wink Tripper", namespace: "mitchpond", author: "Mitch Pond") {
20 capability "Contact Sensor"
22 capability "Configuration"
25 attribute "tamper", "string"
30 fingerprint endpointId: "01", profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,0500,0020,0B05", outClusters: "0003,0019"
36 // UI tile definitions
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")
43 valueTile("battery", "device.battery", decoration: "flat") {
44 state "battery", label:'${currentValue}% battery', unit:""
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"
53 details(["contact","battery","tamper"])
57 // Parse incoming device messages to generate events
58 def parse(String description) {
59 //log.debug "description: $description"
62 if (description?.startsWith('catchall:')) {
63 results = parseCatchAllMessage(description)
65 else if (description?.startsWith('read attr -')) {
66 results = parseReportAttributeMessage(description)
68 else if (description?.startsWith('zone status')) {
69 results = parseIasMessage(description)
72 log.debug "Parse returned $results"
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) }
82 //Initializes device and sets up reporting
84 String zigbeeId = swapEndianHex(device.hub.zigbeeId)
85 log.debug "Confuguring Reporting, IAS CIE, and Bindings."
88 "zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200",
89 "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
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",
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",
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"
104 //Sends IAS Zone Enroll response
105 def enrollResponse() {
106 log.debug "Sending enroll response"
108 "raw 0x500 {01 23 00 00 00}", "delay 200",
109 "send 0x${device.deviceNetworkId} 1 1"
113 private Map parseCatchAllMessage(String description) {
115 def cluster = zigbee.parse(description)
116 if (shouldProcessMessage(cluster)) {
117 switch(cluster.clusterId) {
119 log.debug "Received a catchall message for battery status. This should not happen."
120 results << createEvent(getBatteryResult(cluster.data.last()))
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
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()]
143 //log.debug "Desc Map: $descMap"
147 if (descMap.cluster == "0001" && descMap.attrId == "0020") {
148 log.debug "Received battery level report"
149 results = createEvent(getBatteryResult(Integer.parseInt(descMap.value, 16)))
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)
162 //log.debug(description)
163 if (status & 0b00000001) {results << createEvent(getContactResult('open'))}
164 else if (~status & 0b00000001) results << createEvent(getContactResult('closed'))
166 if (status & 0b00000100) {
167 //log.debug "Tampered"
168 results << createEvent([name: "tamper", value:"tampered"])
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"])
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.
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])
184 else if (~status & 0b00001000) {
185 //log.debug "${linkText} battery OK"
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 **/
195 private getBatteryResult(rawValue) {
196 def linkText = getLinkText(device)
198 def result = [name: 'battery']
200 def volts = rawValue / 10
203 result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
208 def pct = (volts - minVolts) / (maxVolts - minVolts)
209 result.value = Math.min(100, (int) pct * 100)
210 result.descriptionText = "${linkText} battery was ${result.value}%"
217 private Map getContactResult(value) {
218 def linkText = getLinkText(device)
219 def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}"
223 descriptionText: descriptionText
227 //Resets the tamper switch state
228 private resetTamper(){
229 log.debug "Tamper alarm reset."
230 sendEvent([name: "tamper", value:"OK"])
234 new BigInteger(Math.round(value).toString()).toString(16)
237 private String swapEndianHex(String hex) {
238 reverseArray(hex.decodeHex()).encodeHex()
241 private byte[] reverseArray(byte[] array) {
243 int j = array.length - 1;