From e08c0808a56da987e47052b290d813d257b07aa7 Mon Sep 17 00:00:00 2001 From: MMateusiakS Date: Wed, 18 Jul 2018 20:07:40 +0200 Subject: [PATCH] [WWST-626] Add DTH for ZigBee Danalock V3 (#3254) * Add DTH for ZigBee Danalock V3 * Changing checkInterval value and responseMap.data * changing folder name * changing deviceJoinName * Remove redundant files --- .../zigbee-lock-without-codes.src/README.md | 26 ++ .../zigbee-lock-without-codes.groovy | 256 ++++++++++++++++++ 2 files changed, 282 insertions(+) create mode 100644 devicetypes/smartthings/zigbee-lock-without-codes.src/README.md create mode 100644 devicetypes/smartthings/zigbee-lock-without-codes.src/zigbee-lock-without-codes.groovy diff --git a/devicetypes/smartthings/zigbee-lock-without-codes.src/README.md b/devicetypes/smartthings/zigbee-lock-without-codes.src/README.md new file mode 100644 index 00000000000..b932fa44510 --- /dev/null +++ b/devicetypes/smartthings/zigbee-lock-without-codes.src/README.md @@ -0,0 +1,26 @@ +# Danalock ZigBee + +Local Execution + +Works with: + +* [Danalock V3 858125000074](https://danalock.com/products/danalock-v3-smart-lock/) + +## Table of contents + +* [Capabilities](#capabilities) +* [Device Health](#device-health) + +## Capabilities + +* **Configuration** +* **Health Check** +* **Sensor** +* **Battery** +* **Actuator** +* **Lock** +* **Refresh** + +## Device Health +* __122 min__ checkInterval + diff --git a/devicetypes/smartthings/zigbee-lock-without-codes.src/zigbee-lock-without-codes.groovy b/devicetypes/smartthings/zigbee-lock-without-codes.src/zigbee-lock-without-codes.groovy new file mode 100644 index 00000000000..7452d8999ae --- /dev/null +++ b/devicetypes/smartthings/zigbee-lock-without-codes.src/zigbee-lock-without-codes.groovy @@ -0,0 +1,256 @@ +/** + * + * Copyright 2018 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. + * + */ + +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition (name:"ZigBee Lock Without Codes", namespace:"smartthings", author:"SmartThings", vid:"generic-lock-2", mnmn:"SmartThings", runLocally:true, minHubCoreVersion:'000.022.00013', executeCommandsLocally:true) { + capability "Actuator" + capability "Lock" + capability "Refresh" + capability "Sensor" + capability "Battery" + capability "Configuration" + capability "Health Check" + + fingerprint profileId:"0104, 000A", inClusters:"0000, 0001, 0003, 0009, 0020,0101, 0B05", outclusters:"000A, 0019, 0B05", manufacturer:"Danalock", model:"V3-BTZB", deviceJoinName:"Danalock V3 Smart 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:"#00A0DC", 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:"#00A0DC" + 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.refresh", 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"]) + } +} + +private getCLUSTER_POWER() { 0x0001 } +private getCLUSTER_DOORLOCK() { 0x0101 } +private getDOORLOCK_CMD_LOCK_DOOR() { 0x00 } +private getDOORLOCK_CMD_UNLOCK_DOOR() { 0x01 } +private getDOORLOCK_RESPONSE_OPERATION_EVENT() { 0x20 } +private getDOORLOCK_RESPONSE_PROGRAMMING_EVENT() { 0x21 } +private getPOWER_ATTR_BATTERY_PERCENTAGE_REMAINING() { 0x0021 } +private getDOORLOCK_ATTR_LOCKSTATE() { 0x0000 } + +def installed() { + log.debug "Executing installed()" + initialize() +} + +def uninstalled() { + log.debug "Executing uninstalled()" + sendEvent(name:"lockRemoved", value:device.id, isStateChange:true, displayed:false) +} + +def updated() { + try { + if (!state.init || !state.configured) { + state.init = true + def cmds = [] + if (!state.configured) { + cmds << initialize() + } else { + cmds << refresh() + } + + return response(cmds.flatten()) + } + } catch (e) { + log.warn "ZigBee DTH - updated() threw exception:- $e" + } + return null +} + +def ping() { + refresh() +} + +def refresh() { + def cmds = zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE) + return cmds +} + +def configure() { + def cmds = initialize() + return cmds +} + +def initialize() { + log.debug "Executing initialize()" + state.configured = true + sendEvent(name:"checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed:false, data: [protocol:"zigbee", hubHardwareId:device.hub.hardwareID, offlinePingable:"1"]) + + def cmds = zigbee.configureReporting(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE, + DataType.ENUM8, 0, 3600, null) + + zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING,DataType.UINT8, 600, 21600, 0x01) + + zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) + + def allCmds = cmds + refresh() + return allCmds +} + +def lock() { + def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_LOCK_DOOR) + + zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) + + return cmds +} + +def unlock() { + def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_UNLOCK_DOOR) + + zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) + return cmds +} + +def parse(String description) { + def result = null + if (description) { + if (description.startsWith('read attr -')) { + result = parseAttributeResponse(description) + } else { + result = parseCommandResponse(description) + } + } + return result +} + +private def parseAttributeResponse(String description) { + Map descMap = zigbee.parseDescriptionAsMap(description) + log.debug "Executing parseAttributeResponse() with description map:- $descMap" + def result = [] + Map responseMap = [:] + def clusterInt = descMap.clusterInt + def attrInt = descMap.attrInt + def deviceName = device.displayName + responseMap.data = deviceName + + if (clusterInt == CLUSTER_POWER && attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) { + responseMap.name = "battery" + + if (Integer.parseInt(descMap.value, 16) != 255) { + responseMap.value = Math.round(Integer.parseInt(descMap.value, 16) / 2) + responseMap.descriptionText = "Battery is at ${responseMap.value}%" + } + + } else if (clusterInt == CLUSTER_DOORLOCK && attrInt == DOORLOCK_ATTR_LOCKSTATE) { + def value = Integer.parseInt(descMap.value, 16) + responseMap.name = "lock" + if (value == 0) { + responseMap.value = "unknown" + responseMap.descriptionText = "Unknown state" + } else if (value == 1) { + log.debug "locked" + responseMap.value = "locked" + responseMap.descriptionText = "Locked" + } else if (value == 2) { + log.debug "unlocked" + responseMap.value = "unlocked" + responseMap.descriptionText = "Unlocked" + } else { + responseMap.value = "unknown" + responseMap.descriptionText = "Unknown state" + } + } else { + return null + } + result << createEvent(responseMap) + return result +} + +private def parseCommandResponse(String description) { + Map descMap = zigbee.parseDescriptionAsMap(description) + log.debug "Executing parseCommandResponse() with description map:- $descMap" + + def deviceName = device.displayName + def result = [] + Map responseMap = [:] + def data = descMap.data + def cmd = descMap.commandInt + def clusterInt = descMap.clusterInt + responseMap.data = deviceName + + if (clusterInt == CLUSTER_DOORLOCK && (cmd == DOORLOCK_CMD_LOCK_DOOR || cmd == DOORLOCK_CMD_UNLOCK_DOOR)) { + def cmdList = [] + cmdList << "delay 4200" + cmdList << zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE).first() + result << response(cmdList) + } else if (clusterInt == CLUSTER_DOORLOCK && cmd == DOORLOCK_RESPONSE_OPERATION_EVENT) { + def eventSource = Integer.parseInt(data[0], 16) + def eventCode = Integer.parseInt(data[1], 16) + + responseMap.name = "lock" + responseMap.displayed = true + responseMap.isStateChange = true + + if (eventSource == 1) { + responseMap.data = [method: "command"] + } else if (eventSource == 2) { + def desc = "manually" + responseMap.data = [method: "manual"] + } + + switch (eventCode) { + case 1: + responseMap.value = "locked" + responseMap.descriptionText = "Locked ${desc}" + break + case 2: + responseMap.value = "unlocked" + responseMap.descriptionText = "Unlocked ${desc}" + break + default: + break + } + } + + result << createEvent(responseMap) + return result +} + +private Boolean secondsPast(timestamp, seconds) { + if (!(timestamp instanceof Number)) { + if (timestamp instanceof Date) { + timestamp = timestamp.time + } else if ((timestamp instanceof String) && timestamp.isNumber()) { + timestamp = timestamp.toLong() + } else { + return true + } + } + return (now() - timestamp) > (seconds * 1000) +}