/** * Copyright 2018 SmartThings * * Imported for Hubitat Elevation platform by kkossev 2022/11/05 5:23 PM ver. 2.0.0 * * 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 hubitat.zigbee.zcl.DataType import hubitat.zigbee.clusters.iaszone.ZoneStatus metadata { definition (name:"SiHAS ZigBee Lock Without Codes", namespace:"shinasys", author:"SHINA SYSTEM") { capability "Actuator" capability "Lock" // attributes: lock - ENUM ["locked", "unlocked with timeout", "unlocked", "unknown"]; Commands: lock() unlock() capability "Refresh" capability "Sensor" capability "Battery" capability "Configuration" capability "Contact Sensor" fingerprint profileId:"0104", endpointId:"01", inClusters:"0000,0004,0003,0001,0101", outClusters:"0000,0004,0003,0019,0006", model:"DLM-300Z", manufacturer:"ShinaSystem", deviceJoinName:"SiHAS Door Lock" //SiHAS Door Lock preferences { section { input (name: "logEnable", type: "bool", title: "Debug logging", description: "Debug information, useful for troubleshooting. Recommended value is false", defaultValue: false) input (name: "txtEnable", type: "bool", title: "Description text logging", description: "Display sensor states in HE log page. Recommended value is true", defaultValue: true) } } } /* 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 getCLUSTER_IAS_ZONE() { 0x0500 } 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_DOORSTATE() { 0x0003 } private getDOORLOCK_ATTR_LOCKSTATE() { 0x0000 } private getIAS_ATTR_ZONE_STATUS() { 0x0002 } def installed() { if (settings?.logEnable) log.debug "${device.displayName} Executing installed()" initialize() } def uninstalled() { if (settings?.logEnable) log.debug "${device.displayName} 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) { if (settings?.logEnable) log.warn "${device.displayName} ZigBee DTH - updated() threw exception:- $e" } return null } def refresh() { def cmds = [] cmds += zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE) if (isSiHASLock()) cmds += zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_DOORSTATE) return cmds } def configure() { def cmds = initialize() return cmds } def initialize() { if (settings?.logEnable) log.debug "${device.displayName} Executing initialize()" state.configured = true def cmds = [] cmds += zigbee.configureReporting(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE,DataType.ENUM8, 0, 3600, null) cmds += zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING,DataType.UINT8, 600, 21600, 0x01) cmds += zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) if (isSiHASLock()) cmds += zigbee.configureReporting(CLUSTER_DOORLOCK, DOORLOCK_ATTR_DOORSTATE,DataType.ENUM8, 0, 3600, null) cmds += refresh() return cmds } 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 if (description?.startsWith('zone report')) { result = parseIasMessage(description) } else { result = parseCommandResponse(description) } } return result } private def parseAttributeResponse(String description) { Map descMap = zigbee.parseDescriptionAsMap(description) if (settings?.logEnable) log.debug "${device.displayName} 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 "${device.displayName} locked" responseMap.value = "locked" responseMap.descriptionText = "Locked" } else if (value == 2) { log.debug "${device.displayName} unlocked" responseMap.value = "unlocked" responseMap.descriptionText = "Unlocked" } else { responseMap.value = "unknown" responseMap.descriptionText = "Unknown state" } if (responseMap.value) { /* delay this event for a second in the hopes that we get the operation event (which has more info). If we don't get one, then it's okay to send. If we send the event with more info first, the event with less info will be marked as not displayed */ if (settings?.logEnable) log.debug "${device.displayName} Lock attribute report received: ${responseMap.value}. Delaying event." runIn(1, "delayLockEvent", [overwrite: true, forceForLocallyExecuting: true, data: [map: responseMap]]) return [:] } } else if (clusterInt == CLUSTER_DOORLOCK && attrInt == DOORLOCK_ATTR_DOORSTATE) { def value = Integer.parseInt(descMap.value, 16) responseMap.name = "contact" if (value == 0) { responseMap.value = "open" responseMap.descriptionText = "open state" } else if (value == 1) { responseMap.value = "closed" responseMap.descriptionText = "closed state" } } else { return null } result << createEvent(responseMap) return result } def delayLockEvent(data) { if (settings?.logEnable) log.debug "${device.displayName} Sending cached lock operation: ${data.map}" sendEvent(data.map) } private def parseIasMessage(String description) { ZoneStatus zs = zigbee.parseZoneStatus(description) def responseMap = [ name: "battery", value: zs.isBatterySet() ? 5 : 55] return responseMap } private def parseCommandResponse(String description) { Map descMap = zigbee.parseDescriptionAsMap(description) if (settings?.logEnable) log.debug "${device.displayName} 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) } private boolean isSiHASLock() { return true //device.getDataValue("model") == "DLM-300Z" }