From cbd5c91d5224ff388f90443dfda7e21850d80e76 Mon Sep 17 00:00:00 2001 From: Mike Cousins Date: Thu, 22 Oct 2015 13:22:53 -0400 Subject: [PATCH] update keen home smart vent device handler --- .../keen-home-smart-vent.groovy | 195 +++++++++--------- 1 file changed, 101 insertions(+), 94 deletions(-) diff --git a/devicetypes/keen-home/keen-home-smart-vent.src/keen-home-smart-vent.groovy b/devicetypes/keen-home/keen-home-smart-vent.src/keen-home-smart-vent.groovy index c2e0556..232378a 100644 --- a/devicetypes/keen-home/keen-home-smart-vent.src/keen-home-smart-vent.groovy +++ b/devicetypes/keen-home/keen-home-smart-vent.src/keen-home-smart-vent.groovy @@ -1,12 +1,9 @@ -/** - * Keen Home Smart Vent - * - * Author: Keen Home - * Date: 2015-06-23 - */ +// 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: "Gregg Altschul") { + definition (name: "Keen Home Smart Vent", namespace: "Keen Home", author: "Keen Home") { capability "Switch Level" capability "Switch" capability "Configuration" @@ -21,6 +18,7 @@ metadata { command "getBattery" command "getTemperature" command "setZigBeeIdTile" + command "clearObstruction" fingerprint endpoint: "1", profileId: "0104", @@ -42,9 +40,10 @@ metadata { // 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: "switch.off", icon:"st.vents.vent-closed", backgroundColor:"#ff0000" + 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" @@ -206,12 +205,12 @@ private Map makeOnOffResult(rawValue) { private Map makeLevelResult(rawValue) { def linkText = getLinkText(device) - // log.debug "rawValue: ${rawValue}" def value = Integer.parseInt(rawValue, 16) def rangeMax = 254 + // catch obstruction level if (value == 255) { - log.debug "obstructed" + 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. @@ -220,24 +219,9 @@ private Map makeLevelResult(rawValue) { value: "obstructed", descriptionText: "${linkText} is obstructed. Please power cycle." ] - } else if ( device.currentValue("switch") == "obstructed" && - value == 254) { - // When the device is reset after an obstruction, the switch - // state will be obstructed and the value coming from the device - // will be 254. Since we're not using heating/cooling mode from - // the device type handler, we need to bump it down to the lower - // (cooling) range - sendEvent(makeOnOffResult(1)) // clear the obstructed switch state - value = rangeMax } - // else if (device.currentValue("switch") == "off") { - // sendEvent(makeOnOffResult(1)) // turn back on if in off state - // } - - // log.debug "pre-value: ${value}" value = Math.floor(value / rangeMax * 100) - // log.debug "post-value: ${value}" return [ name: "level", @@ -327,35 +311,79 @@ private def makeSerialResult(serial) { value: serial, descriptionText: "${linkText} has serial ${serial}" ] } -/**** COMMAND METHODS ****/ -// def mfgCode() { -// ["zcl mfg-code 0x115B", "delay 200"] -// } +// 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() { - log.debug "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() { - log.debug "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 {}" } -// does this work? -def toggle() { - log.debug "toggle()" +def clearObstruction() { + def linkText = getLinkText(device) + log.debug "attempting to clear ${linkText} obstruction" - "st cmd 0x${device.deviceNetworkId} 1 6 2 {}" + 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") @@ -363,29 +391,26 @@ def setLevel(value) { else { sendEvent(name: "switch", value: "off", descriptionText: "${linkText} is off by setting level to 0") } - def rangeMax = 254 - def computedLevel = Math.round(value * rangeMax / 100) - log.debug "computedLevel: ${computedLevel}" - def level = new BigInteger(computedLevel.toString()).toString(16) - log.debug "level: ${level}" - - if (level.size() < 2){ - level = '0' + level - } - - "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} 0000}" + 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", @@ -395,12 +420,13 @@ def getPressure() { def getLevel() { log.debug "getLevel()" - // rattr = read attribute - // 0x${} = device net id - // 1 = endpoint - // 8 = cluster id (level control, in this case) - // 0 = attribute within cluster - // sendEvent(name: "level", value: value) + + // 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"] } @@ -425,78 +451,59 @@ def setZigBeeIdTile() { name: "zigbeeId", value: device.zigbeeId, descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ]) - return [ + return [ name: "zigbeeId", value: device.zigbeeId, descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ] } def refresh() { - getOnOff() + + getOnOff() + getLevel() + getTemperature() + getPressure() + getBattery() } -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 -} - -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() -} - def configure() { log.debug "CONFIGURE" - log.debug "zigbeeId: ${device.hub.zigbeeId}" + // get ZigBee ID by hidden tile because that's the only way we can do it setZigBeeIdTile() def configCmds = [ - // binding commands + // 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 - // [cluster] [attr] [type] [min-interval] [max-interval] [min-change] + "zdo bind 0x${device.deviceNetworkId} 1 1 0x0001 {${device.zigbeeId}} {}", "delay 500" - // mike 2015/06/22: preconfigured; see tech spec + // 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", - // mike 2015/06/22: preconfigured; see tech spec + // 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", - // mike 2015/06/22: temp and pressure reports are preconfigured, but - // we'd like to override their settings for our own purposes + // 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 10 60 {0A00}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + // "zcl global send-me-a-report 0x0402 0 0x29 60 60 {0A00}", "delay 200", + // "send 0x${device.deviceNetworkId} 1 1", "delay 1500", - // mike 2015/06/22: use new custom pressure attribute - // pressure - type: int32u, change: 1 = 0.1Pa - "zcl mfg-code 0x115B", "delay 200", - "zcl global send-me-a-report 0x0403 0x20 0x22 10 60 {010000}", "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", - // mike 2015/06/22: preconfigured; see tech spec + // 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",