--- /dev/null
+/**
+ * Quirky/Wink Tripper Contact Sensor
+ *
+ * Copyright 2015 Mitch Pond, 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: "Quirky/Wink Tripper", namespace: "mitchpond", author: "Mitch Pond") {
+
+ capability "Contact Sensor"
+ capability "Battery"
+ capability "Configuration"
+ capability "Sensor"
+
+ attribute "tamper", "string"
+
+ command "configure"
+ command "resetTamper"
+
+ fingerprint endpointId: "01", profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,0500,0020,0B05", outClusters: "0003,0019"
+ }
+
+ // simulator metadata
+ simulator {}
+
+ // UI tile definitions
+ tiles {
+ standardTile("contact", "device.contact", width: 2, height: 2, canChangeIcon: true) {
+ state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e")
+ state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821")
+ }
+
+ valueTile("battery", "device.battery", decoration: "flat") {
+ state "battery", label:'${currentValue}% battery', unit:""
+ }
+
+ standardTile("tamper", "device.tamper") {
+ state "OK", label: "Tamper OK", icon: "st.security.alarm.on", backgroundColor:"#79b821", decoration: "flat"
+ state "tampered", label: "Tampered", action: "resetTamper", icon: "st.security.alarm.off", backgroundColor:"#ffa81e", decoration: "flat"
+ }
+
+ main ("contact")
+ details(["contact","battery","tamper"])
+ }
+}
+
+// Parse incoming device messages to generate events
+def parse(String description) {
+ //log.debug "description: $description"
+
+ def results = []
+ if (description?.startsWith('catchall:')) {
+ results = parseCatchAllMessage(description)
+ }
+ else if (description?.startsWith('read attr -')) {
+ results = parseReportAttributeMessage(description)
+ }
+ else if (description?.startsWith('zone status')) {
+ results = parseIasMessage(description)
+ }
+
+ log.debug "Parse returned $results"
+
+ if (description?.startsWith('enroll request')) {
+ List cmds = enrollResponse()
+ log.debug "enroll response: ${cmds}"
+ results = cmds?.collect { new physicalgraph.device.HubAction(it) }
+ }
+ return results
+}
+
+//Initializes device and sets up reporting
+def configure() {
+ String zigbeeId = swapEndianHex(device.hub.zigbeeId)
+ log.debug "Confuguring Reporting, IAS CIE, and Bindings."
+
+ def cmd = [
+ "zcl global write 0x500 0x10 0xf0 {${zigbeeId}}", "delay 200",
+ "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
+
+ "zcl global send-me-a-report 0x500 0x0012 0x19 0 0xFF {}", "delay 200", //get notified on tamper
+ "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
+
+ "zcl global send-me-a-report 1 0x20 0x20 5 3600 {}", "delay 200", //battery report request
+ "send 0x${device.deviceNetworkId} 1 1", "delay 1500",
+
+ "zdo bind 0x${device.deviceNetworkId} 1 1 0x500 {${device.zigbeeId}} {}", "delay 500",
+ "zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500",
+ "st rattr 0x${device.deviceNetworkId} 1 1 0x20"
+ ]
+ cmd
+}
+
+//Sends IAS Zone Enroll response
+def enrollResponse() {
+ log.debug "Sending enroll response"
+ [
+ "raw 0x500 {01 23 00 00 00}", "delay 200",
+ "send 0x${device.deviceNetworkId} 1 1"
+ ]
+}
+
+private Map parseCatchAllMessage(String description) {
+ def results = [:]
+ def cluster = zigbee.parse(description)
+ if (shouldProcessMessage(cluster)) {
+ switch(cluster.clusterId) {
+ case 0x0001:
+ log.debug "Received a catchall message for battery status. This should not happen."
+ results << createEvent(getBatteryResult(cluster.data.last()))
+ break
+ }
+ }
+
+ return results
+}
+
+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 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"
+
+ def results = []
+
+ if (descMap.cluster == "0001" && descMap.attrId == "0020") {
+ log.debug "Received battery level report"
+ results = createEvent(getBatteryResult(Integer.parseInt(descMap.value, 16)))
+ }
+
+ return results
+}
+
+private parseIasMessage(String description) {
+ List parsedMsg = description.split(' ')
+ String msgCode = parsedMsg[2]
+ int status = Integer.decode(msgCode)
+ def linkText = getLinkText(device)
+
+ def results = []
+ //log.debug(description)
+ if (status & 0b00000001) {results << createEvent(getContactResult('open'))}
+ else if (~status & 0b00000001) results << createEvent(getContactResult('closed'))
+
+ if (status & 0b00000100) {
+ //log.debug "Tampered"
+ results << createEvent([name: "tamper", value:"tampered"])
+ }
+ else if (~status & 0b00000100) {
+ //don't reset the status here as we want to force a manual reset
+ //log.debug "Not tampered"
+ //results << createEvent([name: "tamper", value:"OK"])
+ }
+
+ if (status & 0b00001000) {
+ //battery reporting seems unreliable with these devices. However, they do report when low.
+ //Just in case the battery level reporting has stopped working, we'll at least catch the low battery warning.
+ //
+ //** Commented this out as this is currently conflicting with the battery level report **/
+ //log.debug "${linkText} reports low battery!"
+ //results << createEvent([name: "battery", value: 10])
+ }
+ else if (~status & 0b00001000) {
+ //log.debug "${linkText} battery OK"
+ }
+ //log.debug results
+ return results
+}
+
+//Converts the battery level response into a percentage to display in ST
+//and creates appropriate message for given level
+//**real-world testing with this device shows that 2.4v is about as low as it can go **/
+
+private getBatteryResult(rawValue) {
+ def linkText = getLinkText(device)
+
+ def result = [name: 'battery']
+
+ def volts = rawValue / 10
+ def descriptionText
+ if (volts > 3.5) {
+ result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
+ }
+ else {
+ def minVolts = 2.4
+ 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
+}
+
+
+private Map getContactResult(value) {
+ def linkText = getLinkText(device)
+ def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}"
+ return [
+ name: 'contact',
+ value: value,
+ descriptionText: descriptionText
+ ]
+}
+
+//Resets the tamper switch state
+private resetTamper(){
+ log.debug "Tamper alarm reset."
+ sendEvent([name: "tamper", value:"OK"])
+}
+
+private hex(value) {
+ new BigInteger(Math.round(value).toString()).toString(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
+}