Adding DoorSense To Z-Wave Yale Driver

I have recently purchased a Yale Assure Lock 2 (YRD450-F-ZW3-619). It has a Z-Wave module in it and I have also purchased the Wi-Fi bridge that works with it. I have used the Generic Z-Wave Lock driver and it does not include anything for DoorSense. The source code for that driver is not available so I did a search and found https://raw.githubusercontent.com/hansandersson/hubitat/master/yale-assure-z-wave-lock.groovy source code. This driver worked OK except for lock codes which does not concern me at this time. I have never written a driver so I thought modifying this one would be fun to try. I have added code to the driver and it functions in my custom app when I subscribe to my new attribute DoorSense. Now my question is that if you look at the inline code I added to the routine "zwaveEvent(hubitat.zwave.Command cmd)" at line 962 to provide DoorSense I am VERY sure that this code should be in a separate new zwaveEvent() routine. I could not figure out how to do that and would love for someone to tell me the proper way I should include this functionality. I really appreciate the help.

My modified driver follows:

My Driver

/**

  • Z-Wave Lock
  • Copyright 2015 SmartThings
  • Derivative Work Copyright 2019 Hans Andersson
  • 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: "Yale Assure Z-Wave Lock", namespace: "handersson86", author: "Hans Andersson") {
capability "Actuator"
capability "Lock"
capability "Polling"
capability "Refresh"
capability "Sensor"
capability "Lock Codes"
capability "Battery"
capability "Health Check"
capability "Configuration"

  fingerprint mfr:"0129", prod:"8002", model:"0600", deviceJoinName: "Yale Assure Lock" //YRD416, YRD426, YRD446
  fingerprint mfr:"0129", prod:"8004", model:"0600", deviceJoinName: "Yale Assure Lock Push Button Deadbolt" //YRD216
  fingerprint mfr:"0129", prod:"800B", model:"0F00", deviceJoinName: "Yale Assure Keypad Lever Door Lock" // YRL216-ZW2
  fingerprint mfr:"0129", prod:"800C", model:"0F00", deviceJoinName: "Yale Assure Touchscreen Lever Door Lock" // YRL226-ZW2
  fingerprint mfr:"0129", prod:"8002", model:"1000", deviceJoinName: "Yale Assure Lock" //YRD-ZWM-1

}
}

import hubitat.zwave.commands.doorlockv1.*
import hubitat.zwave.commands.usercodev1.*

/**

  • Called on app installed
    */
    def installed() {
    // Device-Watch pings if no device events received for 1 hour (checkInterval)
    sendEvent(name: "checkInterval", value: 1 * 60 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"])
    scheduleInstalledCheck()
    }

/**

  • Verify that we have actually received the lock's initial states.
  • If not, verify that we have at least requested them or request them,
  • and check again.
    */
    def scheduleInstalledCheck() {
    runIn(120, installedCheck)
    }

def installedCheck() {
if (device.currentState("lock") && device.currentState("battery")) {
unschedule("installedCheck")
} else {
// We might have called updated() or configure() at some point but not have received a reply, so don't flood the network
if (!state.lastLockDetailsQuery || secondsPast(state.lastLockDetailsQuery, 2 * 60)) {
def actions = updated()

  	if (actions) {
  		sendHubCommand(actions.toHubAction())
  	}
  }

  scheduleInstalledCheck()

}
}

/**

  • Called on app uninstalled
    */
    def uninstalled() {
    def deviceName = device.displayName
    log.trace "[DTH] Executing 'uninstalled()' for device $deviceName"
    sendEvent(name: "lockRemoved", value: device.id, isStateChange: true, displayed: false)
    }

/**

  • Executed when the user taps on the 'Done' button on the device settings screen. Sends the values to lock.
  • @return hubAction: The commands to be executed
    */
    def updated() {
    // Device-Watch pings if no device events received for 1 hour (checkInterval)
    sendEvent(name: "checkInterval", value: 1 * 60 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"])

def hubAction = null
try {
def cmds =
if (!device.currentState("lock") || !device.currentState("battery") || !state.configured) {
log.debug "Returning commands for lock operation get and battery get"
if (!state.configured) {
cmds << doConfigure()
}
cmds << refresh()
cmds << reloadAllCodes()
if (!state.MSR) {
cmds << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
}
if (!state.fw) {
cmds << zwave.versionV1.versionGet().format()
}
hubAction = response(delayBetween(cmds, 30*1000))
}
} catch (e) {
log.warn "updated() threw $e"
}
hubAction
}

/**

  • Configures the device to settings needed by SmarthThings at device discovery time

*/
def configure() {
log.trace "[DTH] Executing 'configure()' for device ${device.displayName}"
def cmds = doConfigure()
log.debug "Configure returning with commands := $cmds"
cmds
}

/**

  • Returns the list of commands to be executed when the device is being configured/paired

/
def doConfigure() {
log.trace "[DTH] Executing 'doConfigure()' for device ${device.displayName}"
state.configured = true
def cmds = []
cmds << secure(zwave.doorLockV1.doorLockOperationGet())
cmds << secure(zwave.batteryV1.batteryGet())
cmds = delayBetween(cmds, 30
1000)

state.lastLockDetailsQuery = now()

log.debug "Do configure returning with commands := $cmds"
cmds
}

/**

  • Responsible for parsing incoming device messages to generate events
  • @param description: The incoming description from the device
  • @return result: The list of events to be sent out

*/
def parse(String description) {
log.trace "[DTH] Executing 'parse(String description)' for device ${device.displayName} with description = $description"

def result = null
if (description.startsWith("Err")) {
if (state.sec) {
result = createEvent(descriptionText:description, isStateChange:true, displayed:false)
} else {
result = createEvent(
descriptionText: "This lock failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.",
eventType: "ALERT",
name: "secureInclusion",
value: "failed",
displayed: true,
)
}
} else {
def cmd = zwave.parse(description, [ 0x98: 1, 0x62: 1, 0x63: 1, 0x71: 2, 0x72: 2, 0x80: 1, 0x85: 2, 0x86: 1 ])
if (cmd) {
result = zwaveEvent(cmd)
}
}
log.info "[DTH] parse() - returning result=$result"
result
}

/**

  • Responsible for parsing ConfigurationReport command
  • @param cmd: The ConfigurationReport command to be parsed
  • @return The event(s) to be sent out

*/
def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) {
log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd)' with cmd = $cmd"
return null
}

/**

  • Responsible for parsing SecurityMessageEncapsulation command
  • @param cmd: The SecurityMessageEncapsulation command to be parsed
  • @return The event(s) to be sent out

*/
def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation)' with cmd = $cmd"
def encapsulatedCommand = cmd.encapsulatedCommand([0x62: 1, 0x71: 2, 0x80: 1, 0x85: 2, 0x63: 1, 0x98: 1, 0x86: 1])
if (encapsulatedCommand) {
zwaveEvent(encapsulatedCommand)
}
}

/**

  • Responsible for parsing NetworkKeyVerify command
  • @param cmd: The NetworkKeyVerify command to be parsed
  • @return The event(s) to be sent out

*/
def zwaveEvent(hubitat.zwave.commands.securityv1.NetworkKeyVerify cmd) {
log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.securityv1.NetworkKeyVerify)' with cmd = $cmd"
createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful", isStateChange: true)
}

/**

  • Responsible for parsing SecurityCommandsSupportedReport command
  • @param cmd: The SecurityCommandsSupportedReport command to be parsed
  • @return The event(s) to be sent out

*/
def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) {
log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.securityv1.SecurityCommandsSupportedReport)' with cmd = $cmd"
state.sec = cmd.commandClassSupport.collect { String.format("%02X ", it) }.join()
if (cmd.commandClassControl) {
state.secCon = cmd.commandClassControl.collect { String.format("%02X ", it) }.join()
}
createEvent(name:"secureInclusion", value:"success", descriptionText:"Lock is securely included", isStateChange: true)
}

/**

  • Responsible for parsing DoorLockOperationReport command
  • @param cmd: The DoorLockOperationReport command to be parsed
  • @return The event(s) to be sent out

*/
def zwaveEvent(DoorLockOperationReport cmd) {
log.trace "[DTH] Executing 'zwaveEvent(DoorLockOperationReport)' with cmd = $cmd"
def result =

unschedule("followupStateCheck")
unschedule("stateCheck")

// DoorLockOperationReport is called when trying to read the lock state or when the lock is locked/unlocked from the DTH or the smart app
def map = [ name: "lock" ]
map.data = [ lockName: device.displayName ]
if (cmd.doorLockMode == 0xFF) {
map.value = "locked"
map.descriptionText = "Locked"
} else if (cmd.doorLockMode >= 0x40) {
map.value = "unknown"
map.descriptionText = "Unknown state"
} else if (cmd.doorLockMode == 0x01) {
map.value = "unlocked with timeout"
map.descriptionText = "Unlocked with timeout"
} else {
map.value = "unlocked"
map.descriptionText = "Unlocked"
if (state.assoc != zwaveHubNodeId) {
result << response(secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)))
result << response(zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId))
result << response(secure(zwave.associationV1.associationGet(groupingIdentifier:1)))
}
}
return result ? [createEvent(map), *result] : createEvent(map)
}

def delayLockEvent(data) {
log.debug "Sending cached lock operation: $data.map"
sendEvent(data.map)
}

/**

  • Responsible for parsing AlarmReport command
  • @param cmd: The AlarmReport command to be parsed
  • @return The event(s) to be sent out

*/
def zwaveEvent(hubitat.zwave.commands.alarmv2.AlarmReport cmd) {
log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.alarmv2.AlarmReport)' with cmd = $cmd"
def result =

if (cmd.zwaveAlarmType == 6) {
result = handleAccessAlarmReport(cmd)
} else if (cmd.zwaveAlarmType == 7) {
result = handleBurglarAlarmReport(cmd)
} else if(cmd.zwaveAlarmType == 8) {
result = handleBatteryAlarmReport(cmd)
} else {
result = handleAlarmReportUsingAlarmType(cmd)
}

result = result ?: null
log.debug "[DTH] zwaveEvent(hubitat.zwave.commands.alarmv2.AlarmReport) returning with result = $result"
result
}

/**

  • Responsible for handling Access AlarmReport command
  • @param cmd: The AlarmReport command to be parsed
  • @return The event(s) to be sent out

*/
private def handleAccessAlarmReport(cmd) {
log.trace "[DTH] Executing 'handleAccessAlarmReport' with cmd = $cmd"
def result =
def map = null
def codeID, changeType, lockCodes, codeName
def deviceName = device.displayName
lockCodes = loadLockCodes()
if (1 <= cmd.zwaveAlarmEvent && cmd.zwaveAlarmEvent < 10) {
map = [ name: "lock", value: (cmd.zwaveAlarmEvent & 1) ? "locked" : "unlocked" ]
}
switch(cmd.zwaveAlarmEvent) {
case 1: // Manually locked
map.descriptionText = "Locked manually"
map.data = [ method: (cmd.alarmLevel == 2) ? "keypad" : "manual" ]
break
case 2: // Manually unlocked
map.descriptionText = "Unlocked manually"
map.data = [ method: "manual" ]
break
case 3: // Locked by command
map.descriptionText = "Locked"
map.data = [ method: "command" ]
break
case 4: // Unlocked by command
map.descriptionText = "Unlocked"
map.data = [ method: "command" ]
break
case 5: // Locked with keypad
if (cmd.eventParameter || cmd.alarmLevel) {
codeID = readCodeSlotId(cmd)
codeName = getCodeName(lockCodes, codeID)
map.descriptionText = "Locked by "$codeName""
map.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ]
} else {
map.descriptionText = "Locked manually"
map.data = [ method: "keypad" ]
}
break
case 6: // Unlocked with keypad
if (cmd.eventParameter || cmd.alarmLevel) {
codeID = readCodeSlotId(cmd)
codeName = getCodeName(lockCodes, codeID)
map.descriptionText = "Unlocked by "$codeName""
map.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ]
}
break
case 7:
map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
map.data = [ method: "manual" ]
break
case 8:
map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
map.data = [ method: "command" ]
break
case 9: // Auto locked
map = [ name: "lock", value: "locked", data: [ method: "auto" ] ]
map.descriptionText = "Auto locked"
break
case 0xA:
map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
map.data = [ method: "auto" ]
break
case 0xB:
map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
break
case 0xC: // All user codes deleted
result = allCodesDeletedEvent()
map = [ name: "codeChanged", value: "all deleted", descriptionText: "Deleted all user codes", isStateChange: true ]
map.data = [notify: true, notificationText: "Deleted all user codes in $deviceName at ${location.name}"]
result << createEvent(name: "lockCodes", value: (new groovy.json.JsonOutput()).toJson([:]), displayed: false, descriptionText: "'lockCodes' attribute updated")
break
case 0xD: // User code deleted
if (cmd.eventParameter || cmd.alarmLevel) {
codeID = readCodeSlotId(cmd)
if (lockCodes[codeID.toString()]) {
codeName = getCodeName(lockCodes, codeID)
map = [ name: "codeChanged", value: "$codeID deleted", isStateChange: true ]
map.descriptionText = "Deleted "$codeName""
map.data = [ codeName: codeName, notify: true, notificationText: "Deleted "$codeName" in $deviceName at ${location.name}" ]
result << codeDeletedEvent(lockCodes, codeID)
}
}
break
case 0xE: // Master or user code changed/set
if (cmd.eventParameter || cmd.alarmLevel) {
codeID = readCodeSlotId(cmd)
codeName = getCodeNameFromState(lockCodes, codeID)
changeType = getChangeType(lockCodes, codeID)
map = [ name: "codeChanged", value: "$codeID $changeType", descriptionText: "${getStatusForDescription(changeType)} "$codeName"", isStateChange: true ]
map.data = [ codeName: codeName, notify: true, notificationText: "${getStatusForDescription(changeType)} "$codeName" in $deviceName at ${location.name}" ]
if(!isMasterCode(codeID)) {
result << codeSetEvent(lockCodes, codeID, codeName)
} else {
map.descriptionText = "${getStatusForDescription('set')} "$codeName""
map.data.notificationText = "${getStatusForDescription('set')} "$codeName" in $deviceName at ${location.name}"
}
}
break
case 0xF: // Duplicate Pin-code error
if (cmd.eventParameter || cmd.alarmLevel) {
codeID = readCodeSlotId(cmd)
clearStateForSlot(codeID)
map = [ name: "codeChanged", value: "$codeID failed", descriptionText: "User code is duplicate and not added",
isStateChange: true, data: [isCodeDuplicate: true] ]
}
break
case 0x10: // Tamper Alarm
case 0x13:
map = [ name: "tamper", value: "detected", descriptionText: "Keypad attempts exceed code entry limit", isStateChange: true, displayed: true ]
break
case 0x11: // Keypad busy
map = [ descriptionText: "Keypad is busy" ]
break
case 0x12: // Master code changed
codeName = getCodeNameFromState(lockCodes, 0)
map = [ name: "codeChanged", value: "0 set", descriptionText: "${getStatusForDescription('set')} "$codeName"", isStateChange: true ]
map.data = [ codeName: codeName, notify: true, notificationText: "${getStatusForDescription('set')} "$codeName" in $deviceName at ${location.name}" ]
break
case 0xFE:
// delegating it to handleAlarmReportUsingAlarmType
return handleAlarmReportUsingAlarmType(cmd)
default:
// delegating it to handleAlarmReportUsingAlarmType
return handleAlarmReportUsingAlarmType(cmd)
}

if (map) {
if (map.data) {
map.data.lockName = deviceName
} else {
map.data = [ lockName: deviceName ]
}
result << createEvent(map)
}
result = result.flatten()
result
}

/**

  • Responsible for handling Burglar AlarmReport command
  • @param cmd: The AlarmReport command to be parsed
  • @return The event(s) to be sent out

*/
private def handleBurglarAlarmReport(cmd) {
log.trace "[DTH] Executing 'handleBurglarAlarmReport' with cmd = $cmd"
def result =
def deviceName = device.displayName

def map = [ name: "tamper", value: "detected" ]
map.data = [ lockName: deviceName ]
switch (cmd.zwaveAlarmEvent) {
case 0:
map.value = "clear"
map.descriptionText = "Tamper alert cleared"
break
case 1:
case 2:
map.descriptionText = "Intrusion attempt detected"
break
case 3:
map.descriptionText = "Covering removed"
break
case 4:
map.descriptionText = "Invalid code"
break
default:
// delegating it to handleAlarmReportUsingAlarmType
return handleAlarmReportUsingAlarmType(cmd)
}

result << createEvent(map)
result
}

/**

  • Responsible for handling Battery AlarmReport command
  • @param cmd: The AlarmReport command to be parsed
  • @return The event(s) to be sent out
    */
    private def handleBatteryAlarmReport(cmd) {
    log.trace "[DTH] Executing 'handleBatteryAlarmReport' with cmd = $cmd"
    def result =
    def deviceName = device.displayName
    def map = null
    switch(cmd.zwaveAlarmEvent) {
    case 0x01: //power has been applied, check if the battery level updated
    result << response(secure(zwave.batteryV1.batteryGet()))
    break;
    case 0x0A:
    map = [ name: "battery", value: 1, descriptionText: "Battery level critical", displayed: true, data: [ lockName: deviceName ] ]
    break
    case 0x0B:
    map = [ name: "battery", value: 0, descriptionText: "Battery too low to operate lock", isStateChange: true, displayed: true, data: [ lockName: deviceName ] ]
    break
    default:
    // delegating it to handleAlarmReportUsingAlarmType
    return handleAlarmReportUsingAlarmType(cmd)
    }
    result << createEvent(map)
    result
    }

/**

  • Responsible for handling AlarmReport commands which are ignored by Access & Burglar handlers
  • @param cmd: The AlarmReport command to be parsed
  • @return The event(s) to be sent out

/
private def handleAlarmReportUsingAlarmType(cmd) {
log.trace "[DTH] Executing 'handleAlarmReportUsingAlarmType' with cmd = $cmd"
def result = []
def map = null
def codeID, lockCodes, codeName
def deviceName = device.displayName
lockCodes = loadLockCodes()
switch(cmd.alarmType) {
case 9:
case 17:
map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
break
case 16: // Note: for levers this means it's unlocked, for non-motorized deadbolt, it's just unsecured and might not get unlocked
case 19: // Unlocked with keypad
map = [ name: "lock", value: "unlocked" ]
if (cmd.alarmLevel != null) {
codeID = readCodeSlotId(cmd)
codeName = getCodeName(lockCodes, codeID)
map.isStateChange = true // Non motorized locks, mark state changed since it can be unlocked multiple times
map.descriptionText = "Unlocked by "$codeName""
map.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ]
}
break
case 18: // Locked with keypad
codeID = readCodeSlotId(cmd)
map = [ name: "lock", value: "locked" ]
codeName = getCodeName(lockCodes, codeID)
map.descriptionText = "Locked by "$codeName""
map.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ]
break
case 21: // Manually locked
map = [ name: "lock", value: "locked", data: [ method: (cmd.alarmLevel == 2) ? "keypad" : "manual" ] ]
map.descriptionText = "Locked manually"
break
case 22: // Manually unlocked
map = [ name: "lock", value: "unlocked", data: [ method: "manual" ] ]
map.descriptionText = "Unlocked manually"
break
case 23:
map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
map.data = [ method: "command" ]
break
case 24: // Locked by command
map = [ name: "lock", value: "locked", data: [ method: "command" ] ]
map.descriptionText = "Locked"
break
case 25: // Unlocked by command
map = [ name: "lock", value: "unlocked", data: [ method: "command" ] ]
map.descriptionText = "Unlocked"
break
case 26:
map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
map.data = [ method: "auto" ]
break
case 27: // Auto locked
map = [ name: "lock", value: "locked", data: [ method: "auto" ] ]
map.descriptionText = "Auto locked"
break
case 32: // All user codes deleted
result = allCodesDeletedEvent()
map = [ name: "codeChanged", value: "all deleted", descriptionText: "Deleted all user codes", isStateChange: true ]
map.data = [notify: true, notificationText: "Deleted all user codes in $deviceName at ${location.name}"]
result << createEvent(name: "lockCodes", value: (new groovy.json.JsonOutput()).toJson([:]), displayed: false, descriptionText: "'lockCodes' attribute updated")
break
case 33: // User code deleted
codeID = readCodeSlotId(cmd)
if (lockCodes[codeID.toString()]) {
codeName = getCodeName(lockCodes, codeID)
map = [ name: "codeChanged", value: "$codeID deleted", isStateChange: true ]
map.descriptionText = "Deleted "$codeName""
map.data = [ codeName: codeName, notify: true, notificationText: "Deleted "$codeName" in $deviceName at ${location.name}" ]
result << codeDeletedEvent(lockCodes, codeID)
}
break
case 38: // Non Access
map = [ descriptionText: "A Non Access Code was entered at the lock", isStateChange: true ]
break
case 13:
case 112: // Master or user code changed/set
codeID = readCodeSlotId(cmd)
codeName = getCodeNameFromState(lockCodes, codeID)
def changeType = getChangeType(lockCodes, codeID)
map = [ name: "codeChanged", value: "$codeID $changeType", descriptionText:
"${getStatusForDescription(changeType)} "$codeName"", isStateChange: true ]
map.data = [ codeName: codeName, notify: true, notificationText: "${getStatusForDescription(changeType)} "$codeName" in $deviceName at ${location.name}" ]
if(!isMasterCode(codeID)) {
result << codeSetEvent(lockCodes, codeID, codeName)
} else {
map.descriptionText = "${getStatusForDescription('set')} "$codeName""
map.data.notificationText = "${getStatusForDescription('set')} "$codeName" in $deviceName at ${location.name}"
}
break
case 34:
case 113: // Duplicate Pin-code error
codeID = readCodeSlotId(cmd)
clearStateForSlot(codeID)
map = [ name: "codeChanged", value: "$codeID failed", descriptionText: "User code is duplicate and not added",
isStateChange: true, data: [isCodeDuplicate: true] ]
break
case 130: // Batteries replaced
map = [ descriptionText: "Batteries replaced", isStateChange: true ]
break
case 131: // Disabled user entered at keypad
map = [ descriptionText: "Code ${cmd.alarmLevel} is disabled", isStateChange: false ]
break
case 161: // Tamper Alarm
if (cmd.alarmLevel == 2) {
map = [ name: "tamper", value: "detected", descriptionText: "Front escutcheon removed", isStateChange: true ]
} else {
map = [ name: "tamper", value: "detected", descriptionText: "Keypad attempts exceed code entry limit", isStateChange: true, displayed: true ]
}
break
case 167: // Low Battery Alarm
if (!state.lastbatt || now() - state.lastbatt > 12
60601000) {
map = [ descriptionText: "Battery low", isStateChange: true ]
result << response(secure(zwave.batteryV1.batteryGet()))
} else {
map = [ name: "battery", value: device.currentValue("battery"), descriptionText: "Battery low", isStateChange: true ]
}
break
case 168: // Critical Battery Alarms
map = [ name: "battery", value: 1, descriptionText: "Battery level critical", displayed: true ]
break
case 169: // Battery too low to operate
map = [ name: "battery", value: 0, descriptionText: "Battery too low to operate lock", isStateChange: true, displayed: true ]
break
default:
map = [ displayed: false, descriptionText: "Alarm event ${cmd.alarmType} level ${cmd.alarmLevel}" ]
break
}

if (map) {
if (map.data) {
map.data.lockName = deviceName
} else {
map.data = [ lockName: deviceName ]
}
result << createEvent(map)
}
result = result.flatten()
result
}

/**

  • Responsible for parsing UserCodeReport command
  • @param cmd: The UserCodeReport command to be parsed
  • @return The event(s) to be sent out

*/
def zwaveEvent(UserCodeReport cmd) {
log.trace "[DTH] Executing 'zwaveEvent(UserCodeReport)' with userIdentifier: ${cmd.userIdentifier} and status: ${cmd.userIdStatus}"
def result =
// cmd.userIdentifier seems to be an int primitive type
def codeID = cmd.userIdentifier.toString()
def lockCodes = loadLockCodes()
def map = [ name: "codeChanged", isStateChange: true ]
def deviceName = device.displayName
def userIdStatus = cmd.userIdStatus

if (userIdStatus == UserCodeReport.USER_ID_STATUS_OCCUPIED ||
(userIdStatus == UserCodeReport.USER_ID_STATUS_STATUS_NOT_AVAILABLE && cmd.user)) {

  def codeName = getCodeName(lockCodes, codeID)
  def changeType = getChangeType(lockCodes, codeID)
  if (!lockCodes[codeID]) {
  	result << codeSetEvent(lockCodes, codeID, codeName)
  } else {
  	map.displayed = false
  }
  map.value = "$codeID $changeType"
  map.descriptionText = "${getStatusForDescription(changeType)} \"$codeName\""
  map.data = [ codeName: codeName, lockName: deviceName ]

} else {
// We are using userIdStatus here because codeID = 0 is reported when user tries to set programming code as the user code
// code is not set
if (lockCodes[codeID]) {
def codeName = getCodeName(lockCodes, codeID)
map.value = "$codeID deleted"
map.descriptionText = "Deleted "$codeName""
map.data = [ codeName: codeName, lockName: deviceName, notify: true, notificationText: "Deleted "$codeName" in $deviceName at ${location.name}" ]
result << codeDeletedEvent(lockCodes, codeID)
} else {
map.value = "$codeID unset"
map.displayed = false
map.data = [ lockName: deviceName ]
}
}

clearStateForSlot(codeID)
result << createEvent(map)

if (codeID.toInteger() == state.checkCode) { // reloadAllCodes() was called, keep requesting the codes in order
if (state.checkCode + 1 > state.codes || state.checkCode >= 8) {
state.remove("checkCode") // done
state["checkCode"] = null
sendEvent(name: "scanCodes", value: "Complete", descriptionText: "Code scan completed", displayed: false)
} else {
state.checkCode = state.checkCode + 1 // get next
result << response(requestCode(state.checkCode))
}
}
if (codeID.toInteger() == state.pollCode) {
if (state.pollCode + 1 > state.codes || state.pollCode >= 15) {
state.remove("pollCode") // done
state["pollCode"] = null
} else {
state.pollCode = state.pollCode + 1
}
}

result = result.flatten()
result
}

/**

  • Responsible for parsing UsersNumberReport command
  • @param cmd: The UsersNumberReport command to be parsed
  • @return The event(s) to be sent out

*/
def zwaveEvent(UsersNumberReport cmd) {
log.trace "[DTH] Executing 'zwaveEvent(UsersNumberReport)' with cmd = $cmd"
def result = [createEvent(name: "maxCodes", value: cmd.supportedUsers, displayed: false)]
state.codes = cmd.supportedUsers
if (state.checkCode) {
if (state.checkCode <= cmd.supportedUsers) {
result << response(requestCode(state.checkCode))
} else {
state.remove("checkCode")
state["checkCode"] = null
}
}
result
}

/**

  • Responsible for parsing AssociationReport command
  • @param cmd: The AssociationReport command to be parsed
  • @return The event(s) to be sent out

*/
def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) {
log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport)' with cmd = $cmd"
def result =
if (cmd.nodeId.any { it == zwaveHubNodeId }) {
state.remove("associationQuery")
state["associationQuery"] = null
result << createEvent(descriptionText: "Is associated")
state.assoc = zwaveHubNodeId
if (cmd.groupingIdentifier == 2) {
result << response(zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:zwaveHubNodeId))
}
} else if (cmd.groupingIdentifier == 1) {
result << response(secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)))
} else if (cmd.groupingIdentifier == 2) {
result << response(zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId))
}
result
}

/**

  • Responsible for parsing TimeGet command
  • @param cmd: The TimeGet command to be parsed
  • @return The event(s) to be sent out

*/
def zwaveEvent(hubitat.zwave.commands.timev1.TimeGet cmd) {
log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.timev1.TimeGet)' with cmd = $cmd"
def result =
def now = new Date().toCalendar()
if(location.timeZone) now.timeZone = location.timeZone
result << createEvent(descriptionText: "Requested time update", displayed: false)
result << response(secure(zwave.timeV1.timeReport(
hourLocalTime: now.get(Calendar.HOUR_OF_DAY),
minuteLocalTime: now.get(Calendar.MINUTE),
secondLocalTime: now.get(Calendar.SECOND)))
)
result
}

/**

  • Responsible for parsing BasicSet command
  • @param cmd: The BasicSet command to be parsed
  • @return The event(s) to be sent out

*/
def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) {
log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet)' with cmd = $cmd"
// DEPRECATED: The old Schlage locks use group 1 for basic control - we don't want that, so unsubscribe from group 1
def result = [ createEvent(name: "lock", value: cmd.value ? "unlocked" : "locked") ]
def cmds = [
zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:zwaveHubNodeId).format(),
"delay 1200",
zwave.associationV1.associationGet(groupingIdentifier:2).format()
]
[result, response(cmds)]
}

/**

  • Responsible for parsing BatteryReport command
  • @param cmd: The BatteryReport command to be parsed
  • @return The event(s) to be sent out

*/
def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) {
log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport)' with cmd = $cmd"
def map = [ name: "battery", unit: "%" ]
if (cmd.batteryLevel == 0xFF) {
map.value = 1
map.descriptionText = "Has a low battery"
} else {
map.value = cmd.batteryLevel
map.descriptionText = "Battery is at ${cmd.batteryLevel}%"
}
state.lastbatt = now()
createEvent(map)
}

/**

  • Responsible for parsing ManufacturerSpecificReport command
  • @param cmd: The ManufacturerSpecificReport command to be parsed
  • @return The event(s) to be sent out

*/
def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport)' with cmd = $cmd"
def result =
def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
updateDataValue("MSR", msr)
result << createEvent(descriptionText: "MSR: $msr", isStateChange: false)
result
}

/**

  • Responsible for parsing VersionReport command
  • @param cmd: The VersionReport command to be parsed
  • @return The event(s) to be sent out

*/
def zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) {
log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport)' with cmd = $cmd"
def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}"
updateDataValue("fw", fw)
if (getDataValue("MSR") == "003B-6341-5044") {
updateDataValue("ver", "${cmd.applicationVersion >> 4}.${cmd.applicationVersion & 0xF}")
}
def text = "${device.displayName}: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}"
createEvent(descriptionText: text, isStateChange: false)
}

/**

  • Responsible for parsing ApplicationBusy command
  • @param cmd: The ApplicationBusy command to be parsed
  • @return The event(s) to be sent out

*/
def zwaveEvent(hubitat.zwave.commands.applicationstatusv1.ApplicationBusy cmd) {
log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.applicationstatusv1.ApplicationBusy)' with cmd = $cmd"
def msg = cmd.status == 0 ? "try again later" :
cmd.status == 1 ? "try again in ${cmd.waitTime} seconds" :
cmd.status == 2 ? "request queued" : "sorry"
createEvent(displayed: true, descriptionText: "Is busy, $msg")
}

/**

  • Responsible for parsing ApplicationRejectedRequest command
  • @param cmd: The ApplicationRejectedRequest command to be parsed
  • @return The event(s) to be sent out

*/
def zwaveEvent(hubitat.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) {
log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.applicationstatusv1.ApplicationRejectedRequest)' with cmd = $cmd"
createEvent(displayed: true, descriptionText: "Rejected the last request")
}

def GetMap(String str) {

  // Finding string length
 int n = str.length();

 // Last character of a string
 char last = str.charAt(n - 1);

//The last character is sometimes missing?!
if (last != ')') {
    str = str + ')'    
}

def ReturnedString = str.replaceFirst("\\(", "\\[")
ReturnedString = ReturnedString.replaceFirst("\\)", "\\]")

def index = ReturnedString.indexOf("[")
                
ReturnedString = ReturnedString.substring(index)

ReturnedMap = evaluate(ReturnedString)

return(ReturnedMap)

}

/**

  • Responsible for parsing zwave command
  • @param cmd: The zwave command to be parsed
  • @return The event(s) to be sent out

*/
def zwaveEvent(hubitat.zwave.Command cmd) {
log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.Command)' with cmd = $cmd"

def cmdMap = GetMap(cmd.toString())

// log.debug "*** cmdMap *** = " + cmdMap

def encapsulatedCmd = cmd.encapsulatedCommand(commandClassVersions)	

// log.debug "*** encapsulatedCmd *** = " + encapsulatedCmd

def Map = GetMap(encapsulatedCmd.toString())

// log.debug "*** encapsulatedMap *** = " + Map

// is this a NotificationReport
if (encapsulatedCmd.toString().indexOf("NotificationReport") == 0) {
    log.trace "got NotificationReport" 
    
    // is this v1AlarmType = 0x2b  (DoorState/DoorSense)
    if (Map.v1AlarmType == 43) {
        // what is the state of DoorSense
        log.trace "got v1AlarmType = 43"
        
        switch (Map.v1AlarmLevel) {
            case 0:  // Door is open
                log.trace "got v1AlarmLevel = 0  (Door Open)"
                sendEvent(name: "DoorSense", value: "open", isStateChange: true)
                break;
            
            case 1:  // Door is closed
                log.trace "got v1AlarmLevel = 1  (Door Closed)"
                sendEvent(name: "DoorSense", value: "closed", isStateChange: true)
                break;
            
            case 2:  // Door Propped (door open for longer than configurable door propped open)
                log.trace "got v1AlarmLevel = 2  (Door Propped)"
                sendEvent(name: "DoorSense", value: "Propped", isStateChange: true)
                break;
            
            default:
                log.trace "NotificationReport Error (Received non supported v1AlarmLevel from lock)"
                break;
        }
    }
}

createEvent(displayed: false, descriptionText: "$cmd")

}

/**

  • Executes lock and then check command with a delay on a lock
    */
    def lockAndCheck(doorLockMode) {
    secureSequence([
    zwave.doorLockV1.doorLockOperationSet(doorLockMode: doorLockMode),
    zwave.doorLockV1.doorLockOperationGet()
    ], 4200)
    }

/**

  • Executes lock command on a lock
    */
    def lock() {
    log.trace "[DTH] Executing lock() for device ${device.displayName}"
    lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_SECURED)
    }

/**

  • Executes unlock command on a lock
    */
    def unlock() {
    log.trace "[DTH] Executing unlock() for device ${device.displayName}"
    lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED)
    }

/**

  • Executes unlock with timeout command on a lock
    */
    def unlockWithTimeout() {
    log.trace "[DTH] Executing unlockWithTimeout() for device ${device.displayName}"
    lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED_WITH_TIMEOUT)
    }

/**

  • PING is used by Device-Watch in attempt to reach the Device
    */
    def ping() {
    log.trace "[DTH] Executing ping() for device ${device.displayName}"
    runIn(30, followupStateCheck)
    secure(zwave.doorLockV1.doorLockOperationGet())
    }

/**

  • Checks the door lock state. Also, schedules checking of door lock state every one hour.
    */
    def followupStateCheck() {
    runEvery1Hour(stateCheck)
    stateCheck()
    }

/**

  • Checks the door lock state
    */
    def stateCheck() {
    sendHubCommand(new hubitat.device.HubAction(secure(zwave.doorLockV1.doorLockOperationGet())))
    }

/**

  • Called when the user taps on the refresh button
    */
    def refresh() {
    log.trace "[DTH] Executing refresh() for device ${device.displayName}"

def cmds = secureSequence([zwave.doorLockV1.doorLockOperationGet(), zwave.batteryV1.batteryGet()])
if (!state.associationQuery) {
cmds << "delay 4200"
cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format() // old Schlage locks use group 2 and don't secure the Association CC
cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
state.associationQuery = now()
} else if (now() - state.associationQuery.toLong() > 9000) {
cmds << "delay 6000"
cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format()
cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))
cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format()
cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
state.associationQuery = now()
}
state.lastLockDetailsQuery = now()

cmds
}

/**

  • Called by the Smart Things platform in case Polling capability is added to the device type
    /
    def poll() {
    log.trace "[DTH] Executing poll() for device ${device.displayName}"
    def cmds = []
    // Only check lock state if it changed recently or we haven't had an update in an hour
    def latest = device.currentState("lock")?.date?.time
    if (!latest || !secondsPast(latest, 6 * 60) || secondsPast(state.lastPoll, 55 * 60)) {
    cmds << secure(zwave.doorLockV1.doorLockOperationGet())
    state.lastPoll = now()
    } else if (!state.lastbatt || now() - state.lastbatt > 53
    60601000) {
    cmds << secure(zwave.batteryV1.batteryGet())
    state.lastbatt = now() //inside-214
    }
    if (state.assoc != zwaveHubNodeId && secondsPast(state.associationQuery, 19 * 60)) {
    cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format()
    cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))
    cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format()
    cmds << "delay 6000"
    cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
    cmds << "delay 6000"
    state.associationQuery = now()
    } else {
    // Only check lock state once per hour
    if (secondsPast(state.lastPoll, 55 * 60)) {
    cmds << secure(zwave.doorLockV1.doorLockOperationGet())
    state.lastPoll = now()
    } else if (!state.MSR) {
    cmds << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
    } else if (!state.fw) {
    cmds << zwave.versionV1.versionGet().format()
    } else if (!device.currentValue("maxCodes")) {
    state.pollCode = 1
    cmds << secure(zwave.userCodeV1.usersNumberGet())
    } else if (state.pollCode && state.pollCode <= state.codes) {
    cmds << requestCode(state.pollCode)
    } else if (!state.lastbatt || now() - state.lastbatt > 536060*1000) {
    cmds << secure(zwave.batteryV1.batteryGet())
    }
    }

if (cmds) {
log.debug "poll is sending ${cmds.inspect()}"
cmds
} else {
// workaround to keep polling from stopping due to lack of activity
sendEvent(descriptionText: "skipping poll", isStateChange: true, displayed: false)
null
}
}

/**

  • Returns the command for user code get
  • @param codeID: The code slot number
  • @return The command for user code get
    */
    def requestCode(codeID) {
    secure(zwave.userCodeV1.userCodeGet(userIdentifier: codeID))
    }

/**

  • API endpoint for server smart app to populate the attributes. Called only when the attributes are not populated.
  • @return The command(s) fired for reading attributes
    */
    def reloadAllCodes() {
    log.trace "[DTH] Executing 'reloadAllCodes()' by ${device.displayName}"
    sendEvent(name: "scanCodes", value: "Scanning", descriptionText: "Code scan in progress", displayed: false)
    def lockCodes = loadLockCodes()
    sendEvent(lockCodesEvent(lockCodes))
    state.checkCode = state.checkCode ?: 1

def cmds =
// Not calling validateAttributes() here because userNumberGet command will be added twice
if (!state.codes) {
// BUG: There might be a bug where Schlage does not return the below number of codes
cmds << secure(zwave.userCodeV1.usersNumberGet())
} else {
sendEvent(name: "maxCodes", value: state.codes, displayed: false)
cmds << requestCode(state.checkCode)
}
if(cmds.size() > 1) {
cmds = delayBetween(cmds, 4200)
}
cmds
}

def getCodes() {
return reloadAllCodes()
}

/**

  • API endpoint for setting the user code length on a lock. This is specific to Schlage locks.
  • @param length: The user code length
  • @returns The command fired for writing the code length attribute
    */
    def setCodeLength(length) {
    return null
    }

/**

  • API endpoint for setting a user code on a lock
  • @param codeID: The code slot number
  • @param code: The code PIN
  • @param codeName: The name of the code
  • @returns cmds: The commands fired for creation and checking of a lock code
    */
    def setCode(codeID, code, codeName = null) {
    if (!code) {
    log.trace "[DTH] Executing 'nameSlot()' by ${this.device.displayName}"
    nameSlot(codeID, codeName)
    return
    }

log.trace "[DTH] Executing 'setCode()' by ${this.device.displayName}"
def strcode = code
if (code instanceof String) {
code = code.toList().findResults { if(it > ' ' && it != ',' && it != '-') it.toCharacter() as Short }
} else {
strcode = code.collect{ it as Character }.join()
}

def strname = (codeName ?: "Code $codeID")
state["setname$codeID"] = strname

def cmds = validateAttributes()
cmds << secure(zwave.userCodeV1.userCodeSet(userIdentifier:codeID, userIdStatus:1, userCode:code))
if(cmds.size() > 1) {
cmds = delayBetween(cmds, 4200)
}
cmds
}

/**

  • Validates attributes and if attributes are not populated, adds the command maps to list of commands
  • @return List of commands or empty list
    */
    def validateAttributes() {
    def cmds =
    if(!device.currentValue("maxCodes")) {
    cmds << secure(zwave.userCodeV1.usersNumberGet())
    }
    log.trace "validateAttributes returning commands list: " + cmds
    cmds
    }

/**

  • API endpoint for setting/deleting multiple user codes on a lock
  • @param codeSettings: The map with code slot numbers and code pins (in case of update)
  • @returns The commands fired for creation and deletion of lock codes
    */
    def updateCodes(codeSettings) {
    log.trace "[DTH] Executing updateCodes() for device ${device.displayName}"
    if(codeSettings instanceof String) codeSettings = (new groovy.json.JsonOutput()).parseJson(codeSettings)
    def set_cmds =
    codeSettings.each { name, updated ->
    if (name.startsWith("code")) {
    def n = name[4..-1].toInteger()
    if (updated && updated.size() >= 4 && updated.size() <= 8) {
    log.debug "Setting code number $n"
    set_cmds << secure(zwave.userCodeV1.userCodeSet(userIdentifier:n, userIdStatus:1, user:updated))
    } else if (updated == null || updated == "" || updated == "0") {
    log.debug "Deleting code number $n"
    set_cmds << deleteCode(n)
    }
    } else log.warn("unexpected entry $name: $updated")
    }
    if (set_cmds) {
    return response(delayBetween(set_cmds, 2200))
    }
    return null
    }

/**

  • Renames an existing lock slot
  • @param codeSlot: The code slot number
  • @param codeName The new name of the code
    */
    void nameSlot(codeSlot, codeName) {
    codeSlot = codeSlot.toString()
    if (!isCodeSet(codeSlot)) {
    return
    }
    def deviceName = device.displayName
    log.trace "[DTH] - Executing nameSlot() for device $deviceName"
    def lockCodes = loadLockCodes()
    def oldCodeName = getCodeName(lockCodes, codeSlot)
    def newCodeName = codeName ?: "Code $codeSlot"
    lockCodes[codeSlot] = newCodeName
    sendEvent(lockCodesEvent(lockCodes))
    sendEvent(name: "codeChanged", value: "$codeSlot renamed", data: [ lockName: deviceName, notify: false, notificationText: "Renamed "$oldCodeName" to "$newCodeName" in $deviceName at ${location.name}" ],
    descriptionText: "Renamed "$oldCodeName" to "$newCodeName"", displayed: true, isStateChange: true)
    }

/**

  • API endpoint for deleting a user code on a lock
  • @param codeID: The code slot number
  • @returns cmds: The command fired for deletion of a lock code
    */
    def deleteCode(codeID) {
    log.trace "[DTH] Executing 'deleteCode()' by ${this.device.displayName}"
    // AlarmReport when a code is deleted manually on the lock
    secureSequence([
    zwave.userCodeV1.userCodeSet(userIdentifier:codeID, userIdStatus:0),
    zwave.userCodeV1.userCodeGet(userIdentifier:codeID)
    ], 4200)
    }

/**

  • Encapsulates a command
  • @param cmd: The command to be encapsulated
  • @returns ret: The encapsulated command
    */
    private secure(hubitat.zwave.Command cmd) {
    zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
    }

/**

  • Encapsulates list of command and adds a delay
  • @param commands: The list of command to be encapsulated
  • @param delay: The delay between commands
  • @returns The encapsulated commands
    */
    private secureSequence(commands, delay=4200) {
    delayBetween(commands.collect{ secure(it) }, delay)
    }

/**

  • Checks if the time elapsed from the provided timestamp is greater than the number of senconds provided
  • @param timestamp: The timestamp
  • @param seconds: The number of seconds
  • @returns true if elapsed time is greater than number of seconds provided, else false
    */
    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)
    }

/**

  • Reads the code name from the 'lockCodes' map
  • @param lockCodes: map with lock code names
  • @param codeID: The code slot number
  • @returns The code name
    */
    private String getCodeName(lockCodes, codeID) {
    if (isMasterCode(codeID)) {
    return "Master Code"
    }
    lockCodes[codeID.toString()] ?: "Code $codeID"
    }

/**

  • Reads the code name from the device state
  • @param lockCodes: map with lock code names
  • @param codeID: The code slot number
  • @returns The code name
    */
    private String getCodeNameFromState(lockCodes, codeID) {
    if (isMasterCode(codeID)) {
    return "Master Code"
    }
    def nameFromLockCodes = lockCodes[codeID.toString()]
    def nameFromState = state["setname$codeID"]
    if(nameFromLockCodes) {
    if(nameFromState) {
    //Updated from smart app
    return nameFromState
    } else {
    //Updated from lock
    return nameFromLockCodes
    }
    } else if(nameFromState) {
    //Set from smart app
    return nameFromState
    }
    //Set from lock
    return "Code $codeID"
    }

/**

  • Check if a user code is present in the 'lockCodes' map
  • @param codeID: The code slot number
  • @returns true if code is present, else false
    */
    private Boolean isCodeSet(codeID) {
    // BUG: Needed to add loadLockCodes to resolve null pointer when using schlage?
    def lockCodes = loadLockCodes()
    lockCodes[codeID.toString()] ? true : false
    }

/**

  • Reads the 'lockCodes' attribute and parses the same
  • @returns Map: The lockCodes map
    */
    private Map loadLockCodes() {
    def stuff = device.currentValue("lockCodes")
    log.debug(stuff)
    // parseJson(device.currentValue("lockCodes") ?: "{}") ?: [:]
    parseJson(stuff ?: "{}") ?: [:]
    }

/**

  • Populates the 'lockCodes' attribute by calling create event
  • @param lockCodes The user codes in a lock
    */
    private Map lockCodesEvent(lockCodes) {
    createEvent(name: "lockCodes", value: (new groovy.json.JsonOutput()).toJson(lockCodes), displayed: false,
    descriptionText: "'lockCodes' attribute updated")
    }

/**

  • Utility function to figure out if code id pertains to master code or not
  • @param codeID - The slot number in which code is set
  • @return - true if slot is for master code, false otherwise
    */
    private boolean isMasterCode(codeID) {
    if(codeID instanceof String) {
    codeID = codeID.toInteger()
    }
    (codeID == 0) ? true : false
    }

/**

  • Creates the event map for user code creation
  • @param lockCodes: The user codes in a lock
  • @param codeID: The code slot number
  • @param codeName: The name of the user code
  • @return The list of events to be sent out
    */
    private def codeSetEvent(lockCodes, codeID, codeName) {
    clearStateForSlot(codeID)
    // codeID seems to be an int primitive type
    lockCodes[codeID.toString()] = (codeName ?: "Code $codeID")
    def result =
    result << lockCodesEvent(lockCodes)
    def codeReportMap = [ name: "codeReport", value: codeID, data: [ code: "" ], isStateChange: true, displayed: false ]
    codeReportMap.descriptionText = "${device.displayName} code $codeID is set"
    result << createEvent(codeReportMap)
    result
    }

/**

  • Creates the event map for user code deletion
  • @param lockCodes: The user codes in a lock
  • @param codeID: The code slot number
  • @return The list of events to be sent out
    */
    private def codeDeletedEvent(lockCodes, codeID) {
    lockCodes.remove("$codeID".toString())
    // not sure if the trigger has done this or not
    clearStateForSlot(codeID)
    def result =
    result << lockCodesEvent(lockCodes)
    def codeReportMap = [ name: "codeReport", value: codeID, data: [ code: "" ], isStateChange: true, displayed: false ]
    codeReportMap.descriptionText = "${device.displayName} code $codeID was deleted"
    result << createEvent(codeReportMap)
    result
    }

/**

  • Creates the event map for all user code deletion

  • @return The List of events to be sent out
    */
    private def allCodesDeletedEvent() {
    def result =
    def lockCodes = loadLockCodes()
    def deviceName = device.displayName
    lockCodes.each { id, code ->
    result << createEvent(name: "codeReport", value: id, data: [ code: "" ], descriptionText: "code $id was deleted",
    displayed: false, isStateChange: true)

    def codeName = code
    result << createEvent(name: "codeChanged", value: "$id deleted", data: [ codeName: codeName, lockName: deviceName,
    notify: true, notificationText: "Deleted "$codeName" in $deviceName at ${location.name}" ],
    descriptionText: "Deleted "$codeName"",
    displayed: true, isStateChange: true)
    clearStateForSlot(id)
    }
    result
    }

/**

  • Checks if a change type is set or update
  • @param lockCodes: The user codes in a lock
  • @param codeID The code slot number
  • @return "set" or "update" basis the presence of the code id in the lockCodes map
    */
    private def getChangeType(lockCodes, codeID) {
    def changeType = "set"
    if (lockCodes[codeID.toString()]) {
    changeType = "changed"
    }
    changeType
    }

/**

  • Method to obtain status for descriptuion based on change type
  • @param changeType: Either "set" or "changed"
  • @return "Added" for "set", "Updated" for "changed", "" otherwise
    */
    private def getStatusForDescription(changeType) {
    if("set" == changeType) {
    return "Added"
    } else if("changed" == changeType) {
    return "Updated"
    }
    //Don't return null as it cause trouble
    return ""
    }

/**

  • Clears the code name and pin from the state basis the code slot number
  • @param codeID: The code slot number
    */
    def clearStateForSlot(codeID) {
    state.remove("setname$codeID")
    state["setname$codeID"] = null
    }

/**

  • Generic function for reading code Slot ID from AlarmReport command
  • @param cmd: The AlarmReport command
  • @return user code slot id
    */
    def readCodeSlotId(hubitat.zwave.commands.alarmv2.AlarmReport cmd) {
    if(cmd.numberOfEventParameters == 1) {
    return cmd.eventParameter[0]
    } else if(cmd.numberOfEventParameters >= 3) {
    return cmd.eventParameter[2]
    }
    return cmd.alarmLevel
    }

def logsOff(){
log.warn "debug logging disabled..."
}

Update 6 hours later:
When I open and close the door I do see in the device page under Current States the "DoorSense" properly showing open and closed. I also see when I manually or with the app lock and unlock the status is reported on the device page correctly. But, when I select lock and unlock from the device page is does not work. When I select lock on the device page I do see in the log "[DTH] Executing lock() for device Yale Assure 2 - Front Door" (from line 1025) but the lock does not respond and no error reported.

I found out that the driver I was trying to modify did not comply with S2 security. I found a few articles in the Hubitat doc's and was able to add the Supervision class and zwaveSecureEncap() and now I have Lock() and Unlock() working. When I try to get the lock codes I am getting an error. It would seems likely that the lock is sending back the data encoded for S2. If that is true how do I convert the data to be decoded so I can use it? Thanks

All the information you need should be in the UserCodeReport. What are you seeing in the places where that command (report) is handled in your code?

The driver page has a button "getCodes" that goes to that method in the driver and eventually executes

private Map loadLockCodes() {
def stuff = device.currentValue("lockCodes")
log.debug(stuff)

parseJson(stuff ?: "{}") ?: [:]
}

The error I get when the last line is executed is

[dev:322](http://192.168.7.26/logs#)2024-11-17 02:15:14.593 PM[error](http://192.168.7.26/logs#)groovy.json.JsonException: A JSON payload should start with an openning curly brace '{' or an openning square bracket '['. Instead, '7' was found on line: 1, column: 1 on line 1464 (method getCodes)

Without being able to see what that actual line is, a reasonable guess is that stuff isn't JSON, and stuff is set to the value of the lockCodes attribute on the device, so whatever that is isn't JSON. You're logging the value of stuff in that code, it appears, so that should also tell you for sure.

But this is a bit different from the original question, or at least what I assumed it was: how to get the lock codes from the lock. I'm not sure what the method above is doing in your driver. In general, the approach for that would be to do a userCodeGet() for each code (I'd suggest one at a time to avoid flooding the network, doing the next when you hear back from the current/previous -- you'll see some drivers use a state variable to track this progress). Then the UserCodeReport, what I mentioned above (and which should come back after doing this, handled as part of parse(), likely as an overloaded method you'd call from somewhere within there as is common in Z-Wave drivers -- in case that isn't apparent), would have the data for that code.

It's possible your driver is already doing that somewhere.

Another thing that might be happening: if lock code encryption is enabled or if the driver is calling encrypy() on the value of lockCodes before setting it (this is the only time built-in drivers will do it, but a driver could do this any way it wants to, really...), you might just need to run decrypt() on that data. This is most likely if the value of lockCodes looks like nonsense — whatever yours is, it's apparently not JSON, so this is another possibility. But the data still needs to come from somewhere, and the snippet above can't be it.

I added stuff2 = decrypt(stuff) and executed log.debug(stuff2) and now see the stuff that I know and love! I actually see the codes in a map. This code is a driver that worked (I believe) years ago and does not comply with S2. I am trying to update it. So at some time the parseJson() worked I think?! I believe that now I have decoded key codes I can finish off the driver... I will soon see.
As for the UserCodeReport I do see a routine in the driver for that but don't believe I have ever seen it executed. How does that get executed?

Updated a few minutes later:

I just commented out the parseJson() line and for the first time I see the trace

[DTH] Executing 'zwaveEvent(UserCodeReport)' with userIdentifier: 1 and status: 1

It has the following error

2024-11-17 02:51:06.722 PM[error](http://192.168.7.26/logs#)java.lang.NullPointerException: Cannot get property '1' on null object on line 1407 (method parse)

I will try to figure it out and if I can't I will beg for some more help. I really appreciate your responses... Thanks

Assuming it works like a normal Z-Wave driver, what happens is that such a report would come from the device to your hub, which means it would hit the parse() method. Then that method will parse it into one of the Z-Wave classes (or not), and call one of your overloaded methods that handles anything the driver cares about, such as the one you're seeing that handles UserCodeReport.

As for when such a report would actually come in: that's up to the device, but it's unlikely to happen on its own (maybe if you made a change of codes on the lock itself; I don't know what the norm is for that, nor do I find that preferable as a user compared to using the hub for this, so I really don't know). The other way it should happen is when you do a userCodeGet(), similar to other Z-Wave commands where you can do a "get" and ideally get a "report" back shortly, if needed. The technique I mentioned above is one way a lock driver might choose to handle this if it needs to fetch all lock codes, iterating from the first code to the max, one at a time, and doing a get on each. If you're working with an existing driver, you'll probably see somewhere where it already does that.

Thanks to everyone above that helped me. I have modified a Yale Assure Lock 2 driver I found that was written in 2019 that did not have S2 security concerns in it. I have added that coding (I hope) and it looks like it works. I put in some initial code for DoorSense but could not finish it because my lock no longer will send any Door Sense info to parse() for final coding. The DoorSense coding worked but it needed to be moved to its own zwaveEvent() routine. I added the routine but then found out I couldn't finish coding. Please do a find for "DoorSense" and look at the comments if you want that functionality.

Yale Assure Z-Wave Lock Driver
/**
 * 	Z-Wave Lock
 *
 *  Derivative Work Copyright 2024 William Siggson
 *  (Previous: Derivative Work Copyright 2019 Hans Andersson)
 *
 *  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.
 *
 */

/**
  *  William Siggson added/modified code to allow functionality of the Yale Assure Lock 2 YRD450-F-ZW3-619
  *  that uses Z-Wave S2 security
  *  
  *
  */

metadata {
	definition (name: "Yale Assure Z-Wave Lock", namespace: "Hubi", author: "William Siggson") {
		capability "Actuator"
		capability "Lock"
		capability "Polling"
		capability "Refresh"
		capability "Sensor"
		capability "Lock Codes"
		capability "Battery"
		capability "Health Check"
		capability "Configuration"

		fingerprint mfr:"0129", prod:"8002", model:"0600", deviceJoinName: "Yale Assure Lock" //YRD416, YRD426, YRD446
		fingerprint mfr:"0129", prod:"8004", model:"0600", deviceJoinName: "Yale Assure Lock Push Button Deadbolt" //YRD216
		fingerprint mfr:"0129", prod:"800B", model:"0F00", deviceJoinName: "Yale Assure Keypad Lever Door Lock" // YRL216-ZW2
		fingerprint mfr:"0129", prod:"800C", model:"0F00", deviceJoinName: "Yale Assure Touchscreen Lever Door Lock" // YRL226-ZW2
		fingerprint mfr:"0129", prod:"8002", model:"1000", deviceJoinName: "Yale Assure Lock" //YRD-ZWM-1
		fingerprint mfr:"0129", prod:"8107", model:"1000", deviceJoinName: "Yale Assure Lock 2 Touch" //YRD450-F-ZW3-619
	}
}

import hubitat.zwave.commands.doorlockv1.*
import hubitat.zwave.commands.usercodev1.*
    
/**
 * Called on app installed
 */
def installed() {
	// Device-Watch pings if no device events received for 1 hour (checkInterval)
	sendEvent(name: "checkInterval", value: 1 * 60 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"])
	scheduleInstalledCheck()
}

/**
 * Verify that we have actually received the lock's initial states.
 * If not, verify that we have at least requested them or request them,
 * and check again.
 */
def scheduleInstalledCheck() {
	runIn(120, installedCheck)
}

def installedCheck() {
	if (device.currentState("lock") && device.currentState("battery")) {
		unschedule("installedCheck")
	} else {
		// We might have called updated() or configure() at some point but not have received a reply, so don't flood the network
		if (!state.lastLockDetailsQuery || secondsPast(state.lastLockDetailsQuery, 2 * 60)) {
			def actions = updated()

			if (actions) {
				sendHubCommand(actions.toHubAction())
			}
		}

		scheduleInstalledCheck()
	}
}

/**
 * Called on app uninstalled
 */
def uninstalled() {
	def deviceName = device.displayName
	log.trace "[DTH] Executing 'uninstalled()' for device $deviceName"
	sendEvent(name: "lockRemoved", value: device.id, isStateChange: true, displayed: false)
}

/**
 * Executed when the user taps on the 'Done' button on the device settings screen. Sends the values to lock.
 *
 * @return hubAction: The commands to be executed
 */
def updated() {
	// Device-Watch pings if no device events received for 1 hour (checkInterval)
	sendEvent(name: "checkInterval", value: 1 * 60 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"])

	def hubAction = null
	try {
		def cmds = []
		if (!device.currentState("lock") || !device.currentState("battery") || !state.configured) {
			log.debug "Returning commands for lock operation get and battery get"
			if (!state.configured) {
				cmds << doConfigure()
			}
			cmds << refresh()
			cmds << reloadAllCodes()
			if (!state.MSR) {
				cmds << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
			}
			if (!state.fw) {
				cmds << zwave.versionV1.versionGet().format()
			}
			hubAction = response(delayBetween(cmds, 30*1000))
		}
	} catch (e) {
		log.warn "updated() threw $e"
	}
	hubAction
}

/**
 * Configures the device to settings needed by SmarthThings at device discovery time
 *
 */
def configure() {
	log.trace "[DTH] Executing 'configure()' for device ${device.displayName}"
	def cmds = doConfigure()
	log.debug "Configure returning with commands := $cmds"
	cmds
}

/**
 * Returns the list of commands to be executed when the device is being configured/paired
 *
 */
def doConfigure() {
	log.trace "[DTH] Executing 'doConfigure()' for device ${device.displayName}"
	state.configured = true
	def cmds = []
	cmds << secure(zwave.doorLockV1.doorLockOperationGet())
	cmds << secure(zwave.batteryV1.batteryGet())
	cmds = delayBetween(cmds, 30*1000)

	state.lastLockDetailsQuery = now()

	log.debug "Do configure returning with commands := $cmds"
	cmds
}

def zwaveEvent(hubitat.zwave.commands.supervisionv1.SupervisionGet cmd) {
    log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.supervisionv1.SupervisionGet cmd)' with cmd = $cmd"
    hubitat.zwave.Command encapCmd = cmd.encapsulatedCommand(commandClassVersions) // we defined commandClassVersions above
    if (encapCmd) {
        zwaveEvent(encapCmd)
    }
    sendHubCommand(new hubitat.device.HubAction(zwaveSecureEncap(zwave.supervisionV1.supervisionReport(sessionID: cmd.sessionID, reserved: 0, moreStatusUpdates: false, status: 0xFF, duration: 0).format()), hubitat.device.Protocol.ZWAVE))
} 

/**
 * Responsible for parsing incoming device messages to generate events
 *
 * @param description: The incoming description from the device
 *
 * @return result: The list of events to be sent out
 *
 */
def parse(String description) {
	log.trace "[DTH] Executing 'parse(String description)' for device ${device.displayName} with description = $description"

	def result = null
	if (description.startsWith("Err")) {
//        log.debug "********************** Err "
		if (state.sec) {
			result = createEvent(descriptionText:description, isStateChange:true, displayed:false)
		} else {
			result = createEvent(
					descriptionText: "This lock failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.",
					eventType: "ALERT",
					name: "secureInclusion",
					value: "failed",
					displayed: true,
			)
		}
	} else {
//        log.debug "*** description *** = " + description
		def cmd = zwave.parse(description, [ 0x98: 1, 0x62: 1, 0x63: 1, 0x71: 2, 0x72: 2, 0x80: 1, 0x85: 2, 0x86: 1 ])
//        log.debug "*** cmd *** = " + cmd
		if (cmd) {
			result = zwaveEvent(cmd)
            log.debug "Parsed ${cmd} to ${result.inspect()}"
		}
	}
	log.info "[DTH] parse() - returning result=$result"
	result
}

/**
 * Responsible for parsing ConfigurationReport command
 *
 * @param cmd: The ConfigurationReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd)' with cmd = $cmd"
	return null
}

/**
 * Responsible for parsing SecurityMessageEncapsulation command
 *
 * @param cmd: The SecurityMessageEncapsulation command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation)' with cmd = $cmd"
	def encapsulatedCommand = cmd.encapsulatedCommand([0x62: 1, 0x71: 2, 0x80: 1, 0x85: 2, 0x63: 1, 0x98: 1, 0x86: 1])
	if (encapsulatedCommand) {
		zwaveEvent(encapsulatedCommand)
	}
}

/**
 * Responsible for parsing NetworkKeyVerify command
 *
 * @param cmd: The NetworkKeyVerify command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.securityv1.NetworkKeyVerify cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.securityv1.NetworkKeyVerify)' with cmd = $cmd"
	createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful", isStateChange: true)
}

/**
 * Responsible for parsing SecurityCommandsSupportedReport command
 *
 * @param cmd: The SecurityCommandsSupportedReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.securityv1.SecurityCommandsSupportedReport)' with cmd = $cmd"
	state.sec = cmd.commandClassSupport.collect { String.format("%02X ", it) }.join()
	if (cmd.commandClassControl) {
		state.secCon = cmd.commandClassControl.collect { String.format("%02X ", it) }.join()
	}
	createEvent(name:"secureInclusion", value:"success", descriptionText:"Lock is securely included", isStateChange: true)
}

/**
 * Responsible for parsing DoorLockOperationReport command
 *
 * @param cmd: The DoorLockOperationReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(DoorLockOperationReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(DoorLockOperationReport)' with cmd = $cmd"
	def result = []

	unschedule("followupStateCheck")
	unschedule("stateCheck")

	// DoorLockOperationReport is called when trying to read the lock state or when the lock is locked/unlocked from the DTH or the smart app
//log.debug "****** cmd ****** = " + cmd
	def map = [ name: "lock" ]
	map.data = [ lockName: device.displayName ]
	if (cmd.doorLockMode == 0xFF) {
		map.value = "locked"
		map.descriptionText = "Locked"
	} else if (cmd.doorLockMode >= 0x40) {
		map.value = "unknown"
		map.descriptionText = "Unknown state"
	} else if (cmd.doorLockMode == 0x01) {
		map.value = "unlocked with timeout"
		map.descriptionText = "Unlocked with timeout"
	}  else {
		map.value = "unlocked"
		map.descriptionText = "Unlocked"
		if (state.assoc != zwaveHubNodeId) {
			result << response(secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)))
			result << response(zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId))
			result << response(secure(zwave.associationV1.associationGet(groupingIdentifier:1)))
		}
	}
	return result ? [createEvent(map), *result] : createEvent(map)
}

def delayLockEvent(data) {
	log.debug "Sending cached lock operation: $data.map"
	sendEvent(data.map)
}

/**
 * Responsible for parsing AlarmReport command
 *
 * @param cmd: The AlarmReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.alarmv2.AlarmReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.alarmv2.AlarmReport)' with cmd = $cmd"
	def result = []

	if (cmd.zwaveAlarmType == 6) {
		result = handleAccessAlarmReport(cmd)
	} else if (cmd.zwaveAlarmType == 7) {
		result = handleBurglarAlarmReport(cmd)
	} else if(cmd.zwaveAlarmType == 8) {
		result = handleBatteryAlarmReport(cmd)
	} else {
		result = handleAlarmReportUsingAlarmType(cmd)
	}

	result = result ?: null
	log.debug "[DTH] zwaveEvent(hubitat.zwave.commands.alarmv2.AlarmReport) returning with result = $result"
	result
}

/**
 * Responsible for handling Access AlarmReport command
 *
 * @param cmd: The AlarmReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
private def handleAccessAlarmReport(cmd) {
	log.trace "[DTH] Executing 'handleAccessAlarmReport' with cmd = $cmd"
	def result = []
	def map = null
	def codeID, changeType, lockCodes, codeName
	def deviceName = device.displayName
	lockCodes = loadLockCodes()
	if (1 <= cmd.zwaveAlarmEvent && cmd.zwaveAlarmEvent < 10) {
		map = [ name: "lock", value: (cmd.zwaveAlarmEvent & 1) ? "locked" : "unlocked" ]
	}
	switch(cmd.zwaveAlarmEvent) {
		case 1: // Manually locked
			map.descriptionText = "Locked manually"
			map.data = [ method: (cmd.alarmLevel == 2) ? "keypad" : "manual" ]
			break
		case 2: // Manually unlocked
			map.descriptionText = "Unlocked manually"
			map.data = [ method: "manual" ]
			break
		case 3: // Locked by command
			map.descriptionText = "Locked"
			map.data = [ method: "command" ]
			break
		case 4: // Unlocked by command
			map.descriptionText = "Unlocked"
			map.data = [ method: "command" ]
			break
		case 5: // Locked with keypad
			if (cmd.eventParameter || cmd.alarmLevel) {
				codeID = readCodeSlotId(cmd)
				codeName = getCodeName(lockCodes, codeID)
				map.descriptionText = "Locked by \"$codeName\""
				map.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ]
			} else {
				map.descriptionText = "Locked manually"
				map.data = [ method: "keypad" ]
			}
			break
		case 6: // Unlocked with keypad
			if (cmd.eventParameter || cmd.alarmLevel) {
				codeID = readCodeSlotId(cmd)
				codeName = getCodeName(lockCodes, codeID)
				map.descriptionText = "Unlocked by \"$codeName\""
				map.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ]
			}
			break
		case 7:
			map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
			map.data = [ method: "manual" ]
			break
		case 8:
			map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
			map.data = [ method: "command" ]
			break
		case 9: // Auto locked
			map = [ name: "lock", value: "locked", data: [ method: "auto" ] ]
			map.descriptionText = "Auto locked"
			break
		case 0xA:
			map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
			map.data = [ method: "auto" ]
			break
		case 0xB:
			map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
			break
		case 0xC: // All user codes deleted
			result = allCodesDeletedEvent()
			map = [ name: "codeChanged", value: "all deleted", descriptionText: "Deleted all user codes", isStateChange: true ]
			map.data = [notify: true, notificationText: "Deleted all user codes in $deviceName at ${location.name}"]
			result << createEvent(name: "lockCodes", value: (new groovy.json.JsonOutput()).toJson([:]), displayed: false, descriptionText: "'lockCodes' attribute updated")
			break
		case 0xD: // User code deleted
			if (cmd.eventParameter || cmd.alarmLevel) {
				codeID = readCodeSlotId(cmd)
				if (lockCodes[codeID.toString()]) {
					codeName = getCodeName(lockCodes, codeID)
					map = [ name: "codeChanged", value: "$codeID deleted", isStateChange: true ]
					map.descriptionText = "Deleted \"$codeName\""
					map.data = [ codeName: codeName, notify: true, notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ]
					result << codeDeletedEvent(lockCodes, codeID)
				}
			}
			break
		case 0xE: // Master or user code changed/set
			if (cmd.eventParameter || cmd.alarmLevel) {
				codeID = readCodeSlotId(cmd)
				codeName = getCodeNameFromState(lockCodes, codeID)
				changeType = getChangeType(lockCodes, codeID)
				map = [ name: "codeChanged", value: "$codeID $changeType",  descriptionText: "${getStatusForDescription(changeType)} \"$codeName\"", isStateChange: true ]
				map.data = [ codeName: codeName, notify: true, notificationText: "${getStatusForDescription(changeType)} \"$codeName\" in $deviceName at ${location.name}" ]
				if(!isMasterCode(codeID)) {
					result << codeSetEvent(lockCodes, codeID, codeName)
				} else {
					map.descriptionText = "${getStatusForDescription('set')} \"$codeName\""
					map.data.notificationText = "${getStatusForDescription('set')} \"$codeName\" in $deviceName at ${location.name}"
				}
			}
			break
		case 0xF: // Duplicate Pin-code error
			if (cmd.eventParameter || cmd.alarmLevel) {
				codeID = readCodeSlotId(cmd)
				clearStateForSlot(codeID)
				map = [ name: "codeChanged", value: "$codeID failed", descriptionText: "User code is duplicate and not added",
						isStateChange: true, data: [isCodeDuplicate: true] ]
			}
			break
		case 0x10: // Tamper Alarm
		case 0x13:
			map = [ name: "tamper", value: "detected", descriptionText: "Keypad attempts exceed code entry limit", isStateChange: true, displayed: true ]
			break
		case 0x11: // Keypad busy
			map = [ descriptionText: "Keypad is busy" ]
			break
		case 0x12: // Master code changed
			codeName = getCodeNameFromState(lockCodes, 0)
			map = [ name: "codeChanged", value: "0 set", descriptionText: "${getStatusForDescription('set')} \"$codeName\"", isStateChange: true ]
			map.data = [ codeName: codeName, notify: true, notificationText: "${getStatusForDescription('set')} \"$codeName\" in $deviceName at ${location.name}" ]
			break
		case 0xFE:
			// delegating it to handleAlarmReportUsingAlarmType
			return handleAlarmReportUsingAlarmType(cmd)
		default:
			// delegating it to handleAlarmReportUsingAlarmType
			return handleAlarmReportUsingAlarmType(cmd)
	}

	if (map) {
		if (map.data) {
			map.data.lockName = deviceName
		} else {
			map.data = [ lockName: deviceName ]
		}
		result << createEvent(map)
	}
	result = result.flatten()
	result
}

/**
 * Responsible for handling Burglar AlarmReport command
 *
 * @param cmd: The AlarmReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
private def handleBurglarAlarmReport(cmd) {
	log.trace "[DTH] Executing 'handleBurglarAlarmReport' with cmd = $cmd"
	def result = []
	def deviceName = device.displayName

	def map = [ name: "tamper", value: "detected" ]
	map.data = [ lockName: deviceName ]
	switch (cmd.zwaveAlarmEvent) {
		case 0:
			map.value = "clear"
			map.descriptionText = "Tamper alert cleared"
			break
		case 1:
		case 2:
			map.descriptionText = "Intrusion attempt detected"
			break
		case 3:
			map.descriptionText = "Covering removed"
			break
		case 4:
			map.descriptionText = "Invalid code"
			break
		default:
			// delegating it to handleAlarmReportUsingAlarmType
			return handleAlarmReportUsingAlarmType(cmd)
	}

	result << createEvent(map)
	result
}

/**
 * Responsible for handling Battery AlarmReport command
 *
 * @param cmd: The AlarmReport command to be parsed
 *
 * @return The event(s) to be sent out
 */
private def handleBatteryAlarmReport(cmd) {
	log.trace "[DTH] Executing 'handleBatteryAlarmReport' with cmd = $cmd"
	def result = []
	def deviceName = device.displayName
	def map = null
	switch(cmd.zwaveAlarmEvent) {
		case 0x01: //power has been applied, check if the battery level updated
			result << response(secure(zwave.batteryV1.batteryGet()))
			break;
		case 0x0A:
			map = [ name: "battery", value: 1, descriptionText: "Battery level critical", displayed: true, data: [ lockName: deviceName ] ]
			break
		case 0x0B:
			map = [ name: "battery", value: 0, descriptionText: "Battery too low to operate lock", isStateChange: true, displayed: true, data: [ lockName: deviceName ] ]
			break
		default:
			// delegating it to handleAlarmReportUsingAlarmType
			return handleAlarmReportUsingAlarmType(cmd)
	}
	result << createEvent(map)
	result
}

/**
 * Responsible for handling AlarmReport commands which are ignored by Access & Burglar handlers
 *
 * @param cmd: The AlarmReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
private def handleAlarmReportUsingAlarmType(cmd) {
	log.trace "[DTH] Executing 'handleAlarmReportUsingAlarmType' with cmd = $cmd"
	def result = []
	def map = null
	def codeID, lockCodes, codeName
	def deviceName = device.displayName
	lockCodes = loadLockCodes()
	switch(cmd.alarmType) {
		case 9:
		case 17:
			map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
			break
		case 16: // Note: for levers this means it's unlocked, for non-motorized deadbolt, it's just unsecured and might not get unlocked
		case 19: // Unlocked with keypad
			map = [ name: "lock", value: "unlocked" ]
			if (cmd.alarmLevel != null) {
				codeID = readCodeSlotId(cmd)
				codeName = getCodeName(lockCodes, codeID)
				map.isStateChange = true // Non motorized locks, mark state changed since it can be unlocked multiple times
				map.descriptionText = "Unlocked by \"$codeName\""
				map.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ]
			}
			break
		case 18: // Locked with keypad
			codeID = readCodeSlotId(cmd)
			map = [ name: "lock", value: "locked" ]
			codeName = getCodeName(lockCodes, codeID)
			map.descriptionText = "Locked by \"$codeName\""
			map.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ]
			break
		case 21: // Manually locked
			map = [ name: "lock", value: "locked", data: [ method: (cmd.alarmLevel == 2) ? "keypad" : "manual" ] ]
			map.descriptionText = "Locked manually"
			break
		case 22: // Manually unlocked
			map = [ name: "lock", value: "unlocked", data: [ method: "manual" ] ]
			map.descriptionText = "Unlocked manually"
			break
		case 23:
			map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
			map.data = [ method: "command" ]
			break
		case 24: // Locked by command
			map = [ name: "lock", value: "locked", data: [ method: "command" ] ]
			map.descriptionText = "Locked"
			break
		case 25: // Unlocked by command
			map = [ name: "lock", value: "unlocked", data: [ method: "command" ] ]
			map.descriptionText = "Unlocked"
			break
		case 26:
			map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
			map.data = [ method: "auto" ]
			break
		case 27: // Auto locked
			map = [ name: "lock", value: "locked", data: [ method: "auto" ] ]
			map.descriptionText = "Auto locked"
			break
		case 32: // All user codes deleted
			result = allCodesDeletedEvent()
			map = [ name: "codeChanged", value: "all deleted", descriptionText: "Deleted all user codes", isStateChange: true ]
			map.data = [notify: true, notificationText: "Deleted all user codes in $deviceName at ${location.name}"]
			result << createEvent(name: "lockCodes", value: (new groovy.json.JsonOutput()).toJson([:]), displayed: false, descriptionText: "'lockCodes' attribute updated")
			break
		case 33: // User code deleted
			codeID = readCodeSlotId(cmd)
			if (lockCodes[codeID.toString()]) {
				codeName = getCodeName(lockCodes, codeID)
				map = [ name: "codeChanged", value: "$codeID deleted", isStateChange: true ]
				map.descriptionText = "Deleted \"$codeName\""
				map.data = [ codeName: codeName, notify: true, notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ]
				result << codeDeletedEvent(lockCodes, codeID)
			}
			break
		case 38: // Non Access
			map = [ descriptionText: "A Non Access Code was entered at the lock", isStateChange: true ]
			break
		case 13:
		case 112: // Master or user code changed/set
			codeID = readCodeSlotId(cmd)
			codeName = getCodeNameFromState(lockCodes, codeID)
			def changeType = getChangeType(lockCodes, codeID)
			map = [ name: "codeChanged", value: "$codeID $changeType", descriptionText:
					"${getStatusForDescription(changeType)} \"$codeName\"", isStateChange: true ]
			map.data = [ codeName: codeName, notify: true, notificationText: "${getStatusForDescription(changeType)} \"$codeName\" in $deviceName at ${location.name}" ]
			if(!isMasterCode(codeID)) {
				result << codeSetEvent(lockCodes, codeID, codeName)
			} else {
				map.descriptionText = "${getStatusForDescription('set')} \"$codeName\""
				map.data.notificationText = "${getStatusForDescription('set')} \"$codeName\" in $deviceName at ${location.name}"
			}
			break
		case 34:
		case 113: // Duplicate Pin-code error
			codeID = readCodeSlotId(cmd)
			clearStateForSlot(codeID)
			map = [ name: "codeChanged", value: "$codeID failed", descriptionText: "User code is duplicate and not added",
					isStateChange: true, data: [isCodeDuplicate: true] ]
			break
		case 130:  // Batteries replaced
			map = [ descriptionText: "Batteries replaced", isStateChange: true ]
			break
		case 131: // Disabled user entered at keypad
			map = [ descriptionText: "Code ${cmd.alarmLevel} is disabled", isStateChange: false ]
			break
		case 161: // Tamper Alarm
			if (cmd.alarmLevel == 2) {
				map = [ name: "tamper", value: "detected", descriptionText: "Front escutcheon removed", isStateChange: true ]
			} else {
				map = [ name: "tamper", value: "detected", descriptionText: "Keypad attempts exceed code entry limit", isStateChange: true, displayed: true ]
			}
			break
		case 167: // Low Battery Alarm
			if (!state.lastbatt || now() - state.lastbatt > 12*60*60*1000) {
				map = [ descriptionText: "Battery low", isStateChange: true ]
				result << response(secure(zwave.batteryV1.batteryGet()))
			} else {
				map = [ name: "battery", value: device.currentValue("battery"), descriptionText: "Battery low", isStateChange: true ]
			}
			break
		case 168: // Critical Battery Alarms
			map = [ name: "battery", value: 1, descriptionText: "Battery level critical", displayed: true ]
			break
		case 169: // Battery too low to operate
			map = [ name: "battery", value: 0, descriptionText: "Battery too low to operate lock", isStateChange: true, displayed: true ]
			break
		default:
			map = [ displayed: false, descriptionText: "Alarm event ${cmd.alarmType} level ${cmd.alarmLevel}" ]
			break
	}

	if (map) {
		if (map.data) {
			map.data.lockName = deviceName
		} else {
			map.data = [ lockName: deviceName ]
		}
		result << createEvent(map)
	}
	result = result.flatten()
	result
}

/**
 * Responsible for parsing UserCodeReport command
 *
 * @param cmd: The UserCodeReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(UserCodeReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(UserCodeReport)' with userIdentifier: ${cmd.userIdentifier} and status: ${cmd.userIdStatus}"
	def result = []
	// cmd.userIdentifier seems to be an int primitive type
	def codeID = cmd.userIdentifier.toString()
	def lockCodes = loadLockCodes()
	def map = [ name: "codeChanged", isStateChange: true ]
	def deviceName = device.displayName
	def userIdStatus = cmd.userIdStatus
	if (userIdStatus == UserCodeReport.USER_ID_STATUS_OCCUPIED ||
			(userIdStatus == UserCodeReport.USER_ID_STATUS_STATUS_NOT_AVAILABLE && cmd.user)) {

		def codeName = getCodeName(lockCodes, codeID)
		def changeType = getChangeType(lockCodes, codeID)
		if (!lockCodes[codeID]) {
			result << codeSetEvent(lockCodes, codeID, codeName)
		} else {
			map.displayed = false
		}
		map.value = "$codeID $changeType"
		map.descriptionText = "${getStatusForDescription(changeType)} \"$codeName\""
		map.data = [ codeName: codeName, lockName: deviceName ]
	} else {
		// We are using userIdStatus here because codeID = 0 is reported when user tries to set programming code as the user code
		// code is not set
		if (lockCodes[codeID]) {
			def codeName = getCodeName(lockCodes, codeID)
			map.value = "$codeID deleted"
			map.descriptionText = "Deleted \"$codeName\""
			map.data = [ codeName: codeName, lockName: deviceName, notify: true, notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ]
			result << codeDeletedEvent(lockCodes, codeID)
		} else {
			map.value = "$codeID unset"
			map.displayed = false
			map.data = [ lockName: deviceName ]
		}
	}
	clearStateForSlot(codeID)
	result << createEvent(map)
	if (codeID.toInteger() == state.checkCode) {  // reloadAllCodes() was called, keep requesting the codes in order
		if (state.checkCode + 1 > state.codes || state.checkCode >= 8) {
			state.remove("checkCode")  // done
			state["checkCode"] = null
			sendEvent(name: "scanCodes", value: "Complete", descriptionText: "Code scan completed", displayed: false)
		} else {
			state.checkCode = state.checkCode + 1  // get next
			result << response(requestCode(state.checkCode))
		}
	}
	if (codeID.toInteger() == state.pollCode) {
		if (state.pollCode + 1 > state.codes || state.pollCode >= 15) {
			state.remove("pollCode")  // done
			state["pollCode"] = null
		} else {
			state.pollCode = state.pollCode + 1
		}
	}

	result = result.flatten()
	result
}

/**
 * Responsible for parsing UsersNumberReport command
 *
 * @param cmd: The UsersNumberReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(UsersNumberReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(UsersNumberReport)' with cmd = $cmd"
	def result = [createEvent(name: "maxCodes", value: cmd.supportedUsers, displayed: false)]
	state.codes = cmd.supportedUsers
	if (state.checkCode) {
		if (state.checkCode <= cmd.supportedUsers) {
			result << response(requestCode(state.checkCode))
		} else {
			state.remove("checkCode")
			state["checkCode"] = null
		}
	}
	result
}

/**
 * Responsible for parsing AssociationReport command
 *
 * @param cmd: The AssociationReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport)' with cmd = $cmd"
	def result = []
	if (cmd.nodeId.any { it == zwaveHubNodeId }) {
		state.remove("associationQuery")
		state["associationQuery"] = null
		result << createEvent(descriptionText: "Is associated")
		state.assoc = zwaveHubNodeId
		if (cmd.groupingIdentifier == 2) {
			result << response(zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:zwaveHubNodeId))
		}
	} else if (cmd.groupingIdentifier == 1) {
		result << response(secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)))
	} else if (cmd.groupingIdentifier == 2) {
		result << response(zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId))
	}
	result
}

/**
 * Responsible for parsing TimeGet command
 *
 * @param cmd: The TimeGet command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.timev1.TimeGet cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.timev1.TimeGet)' with cmd = $cmd"
	def result = []
	def now = new Date().toCalendar()
	if(location.timeZone) now.timeZone = location.timeZone
	result << createEvent(descriptionText: "Requested time update", displayed: false)
	result << response(secure(zwave.timeV1.timeReport(
			hourLocalTime: now.get(Calendar.HOUR_OF_DAY),
			minuteLocalTime: now.get(Calendar.MINUTE),
			secondLocalTime: now.get(Calendar.SECOND)))
	)
	result
}

/**
 * Responsible for parsing BasicSet command
 *
 * @param cmd: The BasicSet command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet)' with cmd = $cmd"
	// DEPRECATED: The old Schlage locks use group 1 for basic control - we don't want that, so unsubscribe from group 1
	def result = [ createEvent(name: "lock", value: cmd.value ? "unlocked" : "locked") ]
	def cmds = [
			zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:zwaveHubNodeId).format(),
			"delay 1200",
			zwave.associationV1.associationGet(groupingIdentifier:2).format()
	]
	[result, response(cmds)]
}

/**
 * Responsible for parsing BatteryReport command
 *
 * @param cmd: The BatteryReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport)' with cmd = $cmd"
	def map = [ name: "battery", unit: "%" ]
	if (cmd.batteryLevel == 0xFF) {
		map.value = 1
		map.descriptionText = "Has a low battery"
	} else {
		map.value = cmd.batteryLevel
		map.descriptionText = "Battery is at ${cmd.batteryLevel}%"
	}
	state.lastbatt = now()
	createEvent(map)
}

/**
 * Responsible for parsing ManufacturerSpecificReport command
 *
 * @param cmd: The ManufacturerSpecificReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport)' with cmd = $cmd"
	def result = []
	def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
	updateDataValue("MSR", msr)
	result << createEvent(descriptionText: "MSR: $msr", isStateChange: false)
	result
}

/**
 * Responsible for parsing VersionReport command
 *
 * @param cmd: The VersionReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport)' with cmd = $cmd"
	def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}"
	updateDataValue("fw", fw)
	if (getDataValue("MSR") == "003B-6341-5044") {
		updateDataValue("ver", "${cmd.applicationVersion >> 4}.${cmd.applicationVersion & 0xF}")
	}
	def text = "${device.displayName}: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}"
	createEvent(descriptionText: text, isStateChange: false)
}

/**
 * Responsible for parsing ApplicationBusy command
 *
 * @param cmd: The ApplicationBusy command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.applicationstatusv1.ApplicationBusy cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.applicationstatusv1.ApplicationBusy)' with cmd = $cmd"
	def msg = cmd.status == 0 ? "try again later" :
			cmd.status == 1 ? "try again in ${cmd.waitTime} seconds" :
					cmd.status == 2 ? "request queued" : "sorry"
	createEvent(displayed: true, descriptionText: "Is busy, $msg")
}

/**
 * Responsible for parsing ApplicationRejectedRequest command
 *
 * @param cmd: The ApplicationRejectedRequest command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.applicationstatusv1.ApplicationRejectedRequest)' with cmd = $cmd"
	createEvent(displayed: true, descriptionText: "Rejected the last request")
}

def zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport' with cmd = $cmd"    
}

def zwaveEvent(hubitat.zwave.commands.alarmv1.AlarmReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.alarmv1.AlarmReport cmd)' with cmd = $cmd"    
}

def GetMap(String str) {
    
      // Finding string length
     int n = str.length();
 
     // Last character of a string
     char last = str.charAt(n - 1);
    
    if (last != ')') {
        str = str + ')'    
    }

    log.debug "str in GetMap = " + str
    def ReturnedString = str.replaceFirst("\\(", "\\[")
    ReturnedString = ReturnedString.replaceFirst("\\)", "\\]")
    log.debug "str.replace = " + ReturnedString
 
    def index = ReturnedString.indexOf("[")
    log.debug "index = " + index
                    
    ReturnedString = ReturnedString.substring(index)
    
    ReturnedMap = evaluate(ReturnedString)
    
    return(ReturnedMap)
}

/**
 * Responsible for parsing zwave command
 *
 * @param cmd: The zwave command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.Command cmd) {
//    log.debug "***********************************************************************************"
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.Command)' with cmd = $cmd"
 
    
    
        /* The following "indented" code was a first attempt at adding DoorSense.
         * This code worked but before I could move it to it's own zwaveEvent 
         * routine the lock would not generate anything to the parse() routine.
         * I added two unfinished zwaveEvent routines above:
         *    zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd)
         *    zwaveEvent(hubitat.zwave.commands.alarmv1.AlarmReport cmd)
         * So, I do not believe the following code will funtion anymore.  If you comment
         * out the above routines it will probably work... better yet please move/modify
         * the following code and put it in the proper zwaveEvent() routine.
         */

        try {
            def cmdMap = GetMap(cmd.toString())
        //        log.debug "*** cmdMap *** = " + cmdMap
        
            def encapsulatedCmd = cmd.encapsulatedCommand(commandClassVersions)	
        //        log.debug "*** encapsulatedCmd *** = " + encapsulatedCmd
    
            def Map = GetMap(encapsulatedCmd.toString())
        //        log.debug "*** encapsulatedMap *** = " + Map
        }
        catch (e){
            log.warn "updated() threw $e"
            log.debug "Exception catch in 'zwaveEvent(hubitat.zwave.Command cmd)'"   
        }
        
        // is this a NotificationReport
        if (encapsulatedCmd.toString().indexOf("NotificationReport") == 0) {
            log.debug "got NotificationReport" 
        
            // is this v1AlarmType = 0x2b  (DoorState/DoorSense)
            if (Map.v1AlarmType == 43) {
                // what is the state of DoorSense
                log.trace "got v1AlarmType = 43"
            
                switch (Map.v1AlarmLevel) {
                    case 0:  // Door is open
                        log.trace "got v1AlarmLevel = 0  (Door Open)"
                        sendEvent(name: "DoorSense", value: "open", isStateChange: true)
                        break;
                
                    case 1:  // Door is closed
                        log.trace "got v1AlarmLevel = 1  (Door Closed)"
                        sendEvent(name: "DoorSense", value: "closed", isStateChange: true)
                        break;
                
                    case 2:  // Door Propped (door open for longer than configurable door propped open)
                        log.trace "got v1AlarmLevel = 2  (Door Propped)"
                        sendEvent(name: "DoorSense", value: "Propped", isStateChange: true)
                        break;
                
                    default:
                        log.trace "NotificationReport Error (Received non supported v1AlarmLevel from lock)"
                        break;
                }
            }
        }
    
    
    
    createEvent(displayed: false, descriptionText: "$cmd")
}

/**
 * Executes lock and then check command with a delay on a lock
 */
def lockAndCheck(doorLockMode) {

	secureSequence([
			zwave.doorLockV1.doorLockOperationSet(doorLockMode: doorLockMode),
			zwave.doorLockV1.doorLockOperationGet()
	], 4200)
    
//    def cmd = zwave.doorLockV1.doorLockOperationSet(doorLockMode: doorLockMode)
//    zwaveSecureEncap(cmd)
}

/**
 * Executes lock command on a lock
 */
def lock() {
	log.trace "[DTH] Executing lock() for device ${device.displayName}"
	lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_SECURED)
}

/**
 * Executes unlock command on a lock
 */
def unlock() {
	log.trace "[DTH] Executing unlock() for device ${device.displayName}"
	lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED)
}

/**
 * Executes unlock with timeout command on a lock
 */
def unlockWithTimeout() {
	log.trace "[DTH] Executing unlockWithTimeout() for device ${device.displayName}"
	lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED_WITH_TIMEOUT)
}

/**
 * PING is used by Device-Watch in attempt to reach the Device
 */
def ping() {
	log.trace "[DTH] Executing ping() for device ${device.displayName}"
	runIn(30, followupStateCheck)
	secure(zwave.doorLockV1.doorLockOperationGet())
}

/**
 * Checks the door lock state. Also, schedules checking of door lock state every one hour.
 */
def followupStateCheck() {
	runEvery1Hour(stateCheck)
	stateCheck()
}

/**
 * Checks the door lock state
 */
def stateCheck() {
	sendHubCommand(new hubitat.device.HubAction(secure(zwave.doorLockV1.doorLockOperationGet())))
}

/**
 * Called when the user taps on the refresh button
 */
def refresh() {
	log.trace "[DTH] Executing refresh() for device ${device.displayName}"

	def cmds = secureSequence([zwave.doorLockV1.doorLockOperationGet(), zwave.batteryV1.batteryGet()])
	if (!state.associationQuery) {
		cmds << "delay 4200"
		cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format()  // old Schlage locks use group 2 and don't secure the Association CC
		cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
		state.associationQuery = now()
	} else if (now() - state.associationQuery.toLong() > 9000) {
		cmds << "delay 6000"
		cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format()
		cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))
		cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format()
		cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
		state.associationQuery = now()
	}
	state.lastLockDetailsQuery = now()

	cmds
}

/**
 * Called by the Smart Things platform in case Polling capability is added to the device type
 */
def poll() {
	log.trace "[DTH] Executing poll() for device ${device.displayName}"
	def cmds = []
	// Only check lock state if it changed recently or we haven't had an update in an hour
	def latest = device.currentState("lock")?.date?.time
	if (!latest || !secondsPast(latest, 6 * 60) || secondsPast(state.lastPoll, 55 * 60)) {
		cmds << secure(zwave.doorLockV1.doorLockOperationGet())
		state.lastPoll = now()
	} else if (!state.lastbatt || now() - state.lastbatt > 53*60*60*1000) {
		cmds << secure(zwave.batteryV1.batteryGet())
		state.lastbatt = now()  //inside-214
	}
	if (state.assoc != zwaveHubNodeId && secondsPast(state.associationQuery, 19 * 60)) {
		cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format()
		cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))
		cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format()
		cmds << "delay 6000"
		cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
		cmds << "delay 6000"
		state.associationQuery = now()
	} else {
		// Only check lock state once per hour
		if (secondsPast(state.lastPoll, 55 * 60)) {
			cmds << secure(zwave.doorLockV1.doorLockOperationGet())
			state.lastPoll = now()
		} else if (!state.MSR) {
			cmds << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
		} else if (!state.fw) {
			cmds << zwave.versionV1.versionGet().format()
		} else if (!device.currentValue("maxCodes")) {
			state.pollCode = 1
			cmds << secure(zwave.userCodeV1.usersNumberGet())
		} else if (state.pollCode && state.pollCode <= state.codes) {
			cmds << requestCode(state.pollCode)
		} else if (!state.lastbatt || now() - state.lastbatt > 53*60*60*1000) {
			cmds << secure(zwave.batteryV1.batteryGet())
		}
	}

	if (cmds) {
		log.debug "poll is sending ${cmds.inspect()}"
		cmds
	} else {
		// workaround to keep polling from stopping due to lack of activity
		sendEvent(descriptionText: "skipping poll", isStateChange: true, displayed: false)
		null
	}
}

/**
 * Returns the command for user code get
 *
 * @param codeID: The code slot number
 *
 * @return The command for user code get
 */
def requestCode(codeID) {
	secure(zwave.userCodeV1.userCodeGet(userIdentifier: codeID))
}

/**
 * API endpoint for server smart app to populate the attributes. Called only when the attributes are not populated.
 *
 * @return The command(s) fired for reading attributes
 */
def reloadAllCodes() {
	log.trace "[DTH] Executing 'reloadAllCodes()' by ${device.displayName}"
	sendEvent(name: "scanCodes", value: "Scanning", descriptionText: "Code scan in progress", displayed: false)
	def lockCodes = loadLockCodes()
	sendEvent(lockCodesEvent(lockCodes))
	state.checkCode = state.checkCode ?: 1

	def cmds = []
	// Not calling validateAttributes() here because userNumberGet command will be added twice
	if (!state.codes) {
		// BUG: There might be a bug where Schlage does not return the below number of codes
		cmds << secure(zwave.userCodeV1.usersNumberGet())
	} else {
		sendEvent(name: "maxCodes", value: state.codes, displayed: false)
		cmds << requestCode(state.checkCode)
	}
	if(cmds.size() > 1) {
		cmds = delayBetween(cmds, 4200)
	}
	cmds
}

def getCodes() {
    return reloadAllCodes()
}

/**
 * API endpoint for setting the user code length on a lock. This is specific to Schlage locks.
 *
 * @param length: The user code length
 *
 * @returns The command fired for writing the code length attribute
 */
def setCodeLength(length) {
	return null
}

/**
 * API endpoint for setting a user code on a lock
 *
 * @param codeID: The code slot number
 *
 * @param code: The code PIN
 *
 * @param codeName: The name of the code
 *
 * @returns cmds: The commands fired for creation and checking of a lock code
 */
def setCode(codeID, code, codeName = null) {
	if (!code) {
		log.trace "[DTH] Executing 'nameSlot()' by ${this.device.displayName}"
		nameSlot(codeID, codeName)
		return
	}

	log.trace "[DTH] Executing 'setCode()' by ${this.device.displayName}"
	def strcode = code
	if (code instanceof String) {
		code = code.toList().findResults { if(it > ' ' && it != ',' && it != '-') it.toCharacter() as Short }
	} else {
		strcode = code.collect{ it as Character }.join()
	}

	def strname = (codeName ?: "Code $codeID")
	state["setname$codeID"] = strname

	def cmds = validateAttributes()
	cmds << secure(zwave.userCodeV1.userCodeSet(userIdentifier:codeID, userIdStatus:1, userCode:code))
	if(cmds.size() > 1) {
		cmds = delayBetween(cmds, 4200)
	}
	cmds
}

/**
 * Validates attributes and if attributes are not populated, adds the command maps to list of commands
 * @return List of commands or empty list
 */
def validateAttributes() {
	def cmds = []
	if(!device.currentValue("maxCodes")) {
		cmds << secure(zwave.userCodeV1.usersNumberGet())
	}
	log.trace "validateAttributes returning commands list: " + cmds
	cmds
}

/**
 * API endpoint for setting/deleting multiple user codes on a lock
 *
 * @param codeSettings: The map with code slot numbers and code pins (in case of update)
 *
 * @returns The commands fired for creation and deletion of lock codes
 */
def updateCodes(codeSettings) {
	log.trace "[DTH] Executing updateCodes() for device ${device.displayName}"
	if(codeSettings instanceof String) codeSettings = (new groovy.json.JsonOutput()).parseJson(codeSettings)
	def set_cmds = []
	codeSettings.each { name, updated ->
		if (name.startsWith("code")) {
			def n = name[4..-1].toInteger()
			if (updated && updated.size() >= 4 && updated.size() <= 8) {
				log.debug "Setting code number $n"
				set_cmds << secure(zwave.userCodeV1.userCodeSet(userIdentifier:n, userIdStatus:1, user:updated))
			} else if (updated == null || updated == "" || updated == "0") {
				log.debug "Deleting code number $n"
				set_cmds << deleteCode(n)
			}
		} else log.warn("unexpected entry $name: $updated")
	}
	if (set_cmds) {
		return response(delayBetween(set_cmds, 2200))
	}
	return null
}

/**
 * Renames an existing lock slot
 *
 * @param codeSlot: The code slot number
 *
 * @param codeName The new name of the code
 */
void nameSlot(codeSlot, codeName) {
	codeSlot = codeSlot.toString()
	if (!isCodeSet(codeSlot)) {
		return
	}
	def deviceName = device.displayName
	log.trace "[DTH] - Executing nameSlot() for device $deviceName"
	def lockCodes = loadLockCodes()
	def oldCodeName = getCodeName(lockCodes, codeSlot)
	def newCodeName = codeName ?: "Code $codeSlot"
	lockCodes[codeSlot] = newCodeName
	sendEvent(lockCodesEvent(lockCodes))
	sendEvent(name: "codeChanged", value: "$codeSlot renamed", data: [ lockName: deviceName, notify: false, notificationText: "Renamed \"$oldCodeName\" to \"$newCodeName\" in $deviceName at ${location.name}" ],
			descriptionText: "Renamed \"$oldCodeName\" to \"$newCodeName\"", displayed: true, isStateChange: true)
}

/**
 * API endpoint for deleting a user code on a lock
 *
 * @param codeID: The code slot number
 *
 * @returns cmds: The command fired for deletion of a lock code
 */
def deleteCode(codeID) {
	log.trace "[DTH] Executing 'deleteCode()' by ${this.device.displayName}"
	// AlarmReport when a code is deleted manually on the lock
	secureSequence([
			zwave.userCodeV1.userCodeSet(userIdentifier:codeID, userIdStatus:0),
			zwave.userCodeV1.userCodeGet(userIdentifier:codeID)
	], 4200)
}

/**
 * Encapsulates a command
 *
 * @param cmd: The command to be encapsulated
 *
 * @returns ret: The encapsulated command
 */
private secure(hubitat.zwave.Command cmd) {
//	zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
    zwaveSecureEncap(cmd)    
}

/**
 * Encapsulates list of command and adds a delay
 *
 * @param commands: The list of command to be encapsulated
 *
 * @param delay: The delay between commands
 *
 * @returns The encapsulated commands
 */
private secureSequence(commands, delay=4200) {
	delayBetween(commands.collect{ secure(it) }, delay)
}

/**
 * Checks if the time elapsed from the provided timestamp is greater than the number of senconds provided
 *
 * @param timestamp: The timestamp
 *
 * @param seconds: The number of seconds
 *
 * @returns true if elapsed time is greater than number of seconds provided, else false
 */
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)
}

/**
 * Reads the code name from the 'lockCodes' map
 *
 * @param lockCodes: map with lock code names
 *
 * @param codeID: The code slot number
 *
 * @returns The code name
 */
private String getCodeName(lockCodes, codeID) {
	if (isMasterCode(codeID)) {
		return "Master Code"
	}
	lockCodes[codeID.toString()] ?: "Code $codeID"
}

/**
 * Reads the code name from the device state
 *
 * @param lockCodes: map with lock code names
 *
 * @param codeID: The code slot number
 *
 * @returns The code name
 */
private String getCodeNameFromState(lockCodes, codeID) {
	if (isMasterCode(codeID)) {
		return "Master Code"
	}
	def nameFromLockCodes = lockCodes[codeID.toString()]
	def nameFromState = state["setname$codeID"]
	if(nameFromLockCodes) {
		if(nameFromState) {
			//Updated from smart app
			return nameFromState
		} else {
			//Updated from lock
			return nameFromLockCodes
		}
	} else if(nameFromState) {
		//Set from smart app
		return nameFromState
	}
	//Set from lock
	return "Code $codeID"
}

/**
 * Check if a user code is present in the 'lockCodes' map
 *
 * @param codeID: The code slot number
 *
 * @returns true if code is present, else false
 */
private Boolean isCodeSet(codeID) {
	// BUG: Needed to add loadLockCodes to resolve null pointer when using schlage?
	def lockCodes = loadLockCodes()
	lockCodes[codeID.toString()] ? true : false
}

/**
 * Reads the 'lockCodes' attribute and parses the same
 *
 * @returns Map: The lockCodes map
 */
private Map loadLockCodes() {
    def stuff = device.currentValue("lockCodes")
    def stuff2 = decrypt(stuff)
    //	parseJson(device.currentValue("lockCodes") ?: "{}") ?: [:]
	parseJson(stuff2 ?: "{}") ?: [:]
}

/**
 * Populates the 'lockCodes' attribute by calling create event
 *
 * @param lockCodes The user codes in a lock
 */
private Map lockCodesEvent(lockCodes) {
	createEvent(name: "lockCodes", value: (new groovy.json.JsonOutput()).toJson(lockCodes), displayed: false,
			descriptionText: "'lockCodes' attribute updated")
}

/**
 * Utility function to figure out if code id pertains to master code or not
 *
 * @param codeID - The slot number in which code is set
 * @return - true if slot is for master code, false otherwise
 */
private boolean isMasterCode(codeID) {
	if(codeID instanceof String) {
		codeID = codeID.toInteger()
	}
	(codeID == 0) ? true : false
}

/**
 * Creates the event map for user code creation
 *
 * @param lockCodes: The user codes in a lock
 *
 * @param codeID: The code slot number
 *
 * @param codeName: The name of the user code
 *
 * @return The list of events to be sent out
 */
private def codeSetEvent(lockCodes, codeID, codeName) {
	clearStateForSlot(codeID)
	// codeID seems to be an int primitive type
	lockCodes[codeID.toString()] = (codeName ?: "Code $codeID")
	def result = []
	result << lockCodesEvent(lockCodes)
	def codeReportMap = [ name: "codeReport", value: codeID, data: [ code: "" ], isStateChange: true, displayed: false ]
	codeReportMap.descriptionText = "${device.displayName} code $codeID is set"
	result << createEvent(codeReportMap)
	result
}

/**
 * Creates the event map for user code deletion
 *
 * @param lockCodes: The user codes in a lock
 *
 * @param codeID: The code slot number
 *
 * @return The list of events to be sent out
 */
private def codeDeletedEvent(lockCodes, codeID) {
	lockCodes.remove("$codeID".toString())
	// not sure if the trigger has done this or not
	clearStateForSlot(codeID)
	def result = []
	result << lockCodesEvent(lockCodes)
	def codeReportMap = [ name: "codeReport", value: codeID, data: [ code: "" ], isStateChange: true, displayed: false ]
	codeReportMap.descriptionText = "${device.displayName} code $codeID was deleted"
	result << createEvent(codeReportMap)
	result
}

/**
 * Creates the event map for all user code deletion
 *
 * @return The List of events to be sent out
 */
private def allCodesDeletedEvent() {
	def result = []
	def lockCodes = loadLockCodes()
	def deviceName = device.displayName
	lockCodes.each { id, code ->
		result << createEvent(name: "codeReport", value: id, data: [ code: "" ], descriptionText: "code $id was deleted",
				displayed: false, isStateChange: true)

		def codeName = code
		result << createEvent(name: "codeChanged", value: "$id deleted", data: [ codeName: codeName, lockName: deviceName,
																				 notify: true, notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ],
				descriptionText: "Deleted \"$codeName\"",
				displayed: true, isStateChange: true)
		clearStateForSlot(id)
	}
	result
}

/**
 * Checks if a change type is set or update
 *
 * @param lockCodes: The user codes in a lock
 *
 * @param codeID The code slot number
 *
 * @return "set" or "update" basis the presence of the code id in the lockCodes map
 */
private def getChangeType(lockCodes, codeID) {
	def changeType = "set"
	if (lockCodes[codeID.toString()]) {
		changeType = "changed"
	}
	changeType
}

/**
 * Method to obtain status for descriptuion based on change type
 * @param changeType: Either "set" or "changed"
 * @return "Added" for "set", "Updated" for "changed", "" otherwise
 */
private def getStatusForDescription(changeType) {
	if("set" == changeType) {
		return "Added"
	} else if("changed" == changeType) {
		return "Updated"
	}
	//Don't return null as it cause trouble
	return ""
}

/**
 * Clears the code name and pin from the state basis the code slot number
 *
 * @param codeID: The code slot number
 */
def clearStateForSlot(codeID) {
	state.remove("setname$codeID")
	state["setname$codeID"] = null
}

/**
 * Generic function for reading code Slot ID from AlarmReport command
 * @param cmd: The AlarmReport command
 * @return user code slot id
 */
def readCodeSlotId(hubitat.zwave.commands.alarmv2.AlarmReport cmd) {
	if(cmd.numberOfEventParameters == 1) {
		return cmd.eventParameter[0]
	} else if(cmd.numberOfEventParameters >= 3) {
		return cmd.eventParameter[2]
	}
	return cmd.alarmLevel
}

def logsOff(){
    log.warn "debug logging disabled..."
}

In my last post I stated that DoorSense is no longer working for me so I could not finish the coding for it. I just called Yale Tech Support and they had me do a calibration (An option in the iPhone Yale app). Prior to calling them I did this procedure atleast 3 times. When I did it with them they had me make sure the step that said put the door in "ajar" state was around 6 inches instead of me doing it at about 1 inch. THAT WORKED!? I then could move the code for DoorSense from zwaveEvent(hubitat.zwave.Command cmd) to the routine I added: zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd). Yale Tech Support seemed surprised that DoorSense was working in Z-Wave? So, try the calibration procedure like I stated above and that might make the difference for ya. I have included the final driver below:

/**
 * 	Z-Wave Lock
 *
 *  Derivative Work Copyright 2024 William Siggson
 *  (Previous: Derivative Work Copyright 2019 Hans Andersson)
 *
 *  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.
 *
 */

/**
  *  William Siggson added/modified code to allow functionality of the Yale Assure Lock 2 YRD450-F-ZW3-619
  *  that uses Z-Wave S2 security
  *  
  *
  */

metadata {
	definition (name: "Yale Assure Z-Wave Lock", namespace: "Hubi", author: "William Siggson") {
        
		capability "Actuator"
		capability "Lock"
		capability "Polling"
		capability "Refresh"
		capability "Sensor"
		capability "Lock Codes"
		capability "Battery"
		capability "Health Check"
		capability "Configuration"

		fingerprint mfr:"0129", prod:"8002", model:"0600", deviceJoinName: "Yale Assure Lock" //YRD416, YRD426, YRD446
		fingerprint mfr:"0129", prod:"8004", model:"0600", deviceJoinName: "Yale Assure Lock Push Button Deadbolt" //YRD216
		fingerprint mfr:"0129", prod:"800B", model:"0F00", deviceJoinName: "Yale Assure Keypad Lever Door Lock" // YRL216-ZW2
		fingerprint mfr:"0129", prod:"800C", model:"0F00", deviceJoinName: "Yale Assure Touchscreen Lever Door Lock" // YRL226-ZW2
		fingerprint mfr:"0129", prod:"8002", model:"1000", deviceJoinName: "Yale Assure Lock" //YRD-ZWM-1
		fingerprint mfr:"0129", prod:"8107", model:"1000", deviceJoinName: "Yale Assure Lock 2 Touch" //YRD450-F-ZW3-619
	}
}

import hubitat.zwave.commands.doorlockv1.*
import hubitat.zwave.commands.usercodev1.*
    
/**
 * Called on app installed
 */
def installed() {
	// Device-Watch pings if no device events received for 1 hour (checkInterval)
	sendEvent(name: "checkInterval", value: 1 * 60 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"])
	scheduleInstalledCheck()
}

/**
 * Verify that we have actually received the lock's initial states.
 * If not, verify that we have at least requested them or request them,
 * and check again.
 */
def scheduleInstalledCheck() {
	runIn(120, installedCheck)
}

def installedCheck() {
	if (device.currentState("lock") && device.currentState("battery")) {
		unschedule("installedCheck")
	} else {
		// We might have called updated() or configure() at some point but not have received a reply, so don't flood the network
		if (!state.lastLockDetailsQuery || secondsPast(state.lastLockDetailsQuery, 2 * 60)) {
			def actions = updated()

			if (actions) {
				sendHubCommand(actions.toHubAction())
			}
		}

		scheduleInstalledCheck()
	}
}

/**
 * Called on app uninstalled
 */
def uninstalled() {
	def deviceName = device.displayName
	log.trace "[DTH] Executing 'uninstalled()' for device $deviceName"
	sendEvent(name: "lockRemoved", value: device.id, isStateChange: true, displayed: false)
}

/**
 * Executed when the user taps on the 'Done' button on the device settings screen. Sends the values to lock.
 *
 * @return hubAction: The commands to be executed
 */
def updated() {
	// Device-Watch pings if no device events received for 1 hour (checkInterval)
	sendEvent(name: "checkInterval", value: 1 * 60 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"])

	def hubAction = null
	try {
		def cmds = []
		if (!device.currentState("lock") || !device.currentState("battery") || !state.configured) {
			log.debug "Returning commands for lock operation get and battery get"
			if (!state.configured) {
				cmds << doConfigure()
			}
			cmds << refresh()
			cmds << reloadAllCodes()
			if (!state.MSR) {
				cmds << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
			}
			if (!state.fw) {
				cmds << zwave.versionV1.versionGet().format()
			}
			hubAction = response(delayBetween(cmds, 30*1000))
		}
	} catch (e) {
		log.warn "updated() threw $e"
	}
	hubAction
}

/**
 * Configures the device to settings needed by SmarthThings at device discovery time
 *
 */
def configure() {
	log.trace "[DTH] Executing 'configure()' for device ${device.displayName}"
	def cmds = doConfigure()
	log.debug "Configure returning with commands := $cmds"
	cmds
}

/**
 * Returns the list of commands to be executed when the device is being configured/paired
 *
 */
def doConfigure() {
	log.trace "[DTH] Executing 'doConfigure()' for device ${device.displayName}"
	state.configured = true
	def cmds = []
	cmds << secure(zwave.doorLockV1.doorLockOperationGet())
	cmds << secure(zwave.batteryV1.batteryGet())
	cmds = delayBetween(cmds, 30*1000)

	state.lastLockDetailsQuery = now()

	log.debug "Do configure returning with commands := $cmds"
	cmds
}

def zwaveEvent(hubitat.zwave.commands.supervisionv1.SupervisionGet cmd) {
    log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.supervisionv1.SupervisionGet cmd)' with cmd = $cmd"
    hubitat.zwave.Command encapCmd = cmd.encapsulatedCommand(commandClassVersions) // we defined commandClassVersions above
    if (encapCmd) {
        zwaveEvent(encapCmd)
    }
    sendHubCommand(new hubitat.device.HubAction(zwaveSecureEncap(zwave.supervisionV1.supervisionReport(sessionID: cmd.sessionID, reserved: 0, moreStatusUpdates: false, status: 0xFF, duration: 0).format()), hubitat.device.Protocol.ZWAVE))
} 

/**
 * Responsible for parsing incoming device messages to generate events
 *
 * @param description: The incoming description from the device
 *
 * @return result: The list of events to be sent out
 *
 */
def parse(String description) {
	log.trace "[DTH] Executing 'parse(String description)' for device ${device.displayName} with description = $description"

	def result = null
	if (description.startsWith("Err")) {
//        log.debug "********************** Err "
		if (state.sec) {
			result = createEvent(descriptionText:description, isStateChange:true, displayed:false)
		} else {
			result = createEvent(
					descriptionText: "This lock failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.",
					eventType: "ALERT",
					name: "secureInclusion",
					value: "failed",
					displayed: true,
			)
		}
	} else {
//        log.debug "*** description *** = " + description
		def cmd = zwave.parse(description, [ 0x98: 1, 0x62: 1, 0x63: 1, 0x71: 2, 0x72: 2, 0x80: 1, 0x85: 2, 0x86: 1 ])
//        log.debug "*** cmd *** = " + cmd
		if (cmd) {
			result = zwaveEvent(cmd)
            log.debug "Parsed ${cmd} to ${result.inspect()}"
		}
	}
	log.info "[DTH] parse() - returning result=$result"
	result
}

/**
 * Responsible for parsing ConfigurationReport command
 *
 * @param cmd: The ConfigurationReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd)' with cmd = $cmd"
	return null
}

/**
 * Responsible for parsing SecurityMessageEncapsulation command
 *
 * @param cmd: The SecurityMessageEncapsulation command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation)' with cmd = $cmd"
	def encapsulatedCommand = cmd.encapsulatedCommand([0x62: 1, 0x71: 2, 0x80: 1, 0x85: 2, 0x63: 1, 0x98: 1, 0x86: 1])
	if (encapsulatedCommand) {
		zwaveEvent(encapsulatedCommand)
	}
}

/**
 * Responsible for parsing NetworkKeyVerify command
 *
 * @param cmd: The NetworkKeyVerify command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.securityv1.NetworkKeyVerify cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.securityv1.NetworkKeyVerify)' with cmd = $cmd"
	createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful", isStateChange: true)
}

/**
 * Responsible for parsing SecurityCommandsSupportedReport command
 *
 * @param cmd: The SecurityCommandsSupportedReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.securityv1.SecurityCommandsSupportedReport)' with cmd = $cmd"
	state.sec = cmd.commandClassSupport.collect { String.format("%02X ", it) }.join()
	if (cmd.commandClassControl) {
		state.secCon = cmd.commandClassControl.collect { String.format("%02X ", it) }.join()
	}
	createEvent(name:"secureInclusion", value:"success", descriptionText:"Lock is securely included", isStateChange: true)
}

/**
 * Responsible for parsing DoorLockOperationReport command
 *
 * @param cmd: The DoorLockOperationReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(DoorLockOperationReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(DoorLockOperationReport)' with cmd = $cmd"
	def result = []

	unschedule("followupStateCheck")
	unschedule("stateCheck")

	// DoorLockOperationReport is called when trying to read the lock state or when the lock is locked/unlocked from the DTH or the smart app
//log.debug "****** cmd ****** = " + cmd
	def map = [ name: "lock" ]
	map.data = [ lockName: device.displayName ]
	if (cmd.doorLockMode == 0xFF) {
		map.value = "locked"
		map.descriptionText = "Locked"
	} else if (cmd.doorLockMode >= 0x40) {
		map.value = "unknown"
		map.descriptionText = "Unknown state"
	} else if (cmd.doorLockMode == 0x01) {
		map.value = "unlocked with timeout"
		map.descriptionText = "Unlocked with timeout"
	}  else {
		map.value = "unlocked"
		map.descriptionText = "Unlocked"
		if (state.assoc != zwaveHubNodeId) {
			result << response(secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)))
			result << response(zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId))
			result << response(secure(zwave.associationV1.associationGet(groupingIdentifier:1)))
		}
	}
	return result ? [createEvent(map), *result] : createEvent(map)
}

def delayLockEvent(data) {
	log.debug "Sending cached lock operation: $data.map"
	sendEvent(data.map)
}

/**
 * Responsible for parsing AlarmReport command
 *
 * @param cmd: The AlarmReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.alarmv2.AlarmReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.alarmv2.AlarmReport)' with cmd = $cmd"
	def result = []

	if (cmd.zwaveAlarmType == 6) {
		result = handleAccessAlarmReport(cmd)
	} else if (cmd.zwaveAlarmType == 7) {
		result = handleBurglarAlarmReport(cmd)
	} else if(cmd.zwaveAlarmType == 8) {
		result = handleBatteryAlarmReport(cmd)
	} else {
		result = handleAlarmReportUsingAlarmType(cmd)
	}

	result = result ?: null
	log.debug "[DTH] zwaveEvent(hubitat.zwave.commands.alarmv2.AlarmReport) returning with result = $result"
	result
}

/**
 * Responsible for handling Access AlarmReport command
 *
 * @param cmd: The AlarmReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
private def handleAccessAlarmReport(cmd) {
	log.trace "[DTH] Executing 'handleAccessAlarmReport' with cmd = $cmd"
	def result = []
	def map = null
	def codeID, changeType, lockCodes, codeName
	def deviceName = device.displayName
	lockCodes = loadLockCodes()
	if (1 <= cmd.zwaveAlarmEvent && cmd.zwaveAlarmEvent < 10) {
		map = [ name: "lock", value: (cmd.zwaveAlarmEvent & 1) ? "locked" : "unlocked" ]
	}
	switch(cmd.zwaveAlarmEvent) {
		case 1: // Manually locked
			map.descriptionText = "Locked manually"
			map.data = [ method: (cmd.alarmLevel == 2) ? "keypad" : "manual" ]
			break
		case 2: // Manually unlocked
			map.descriptionText = "Unlocked manually"
			map.data = [ method: "manual" ]
			break
		case 3: // Locked by command
			map.descriptionText = "Locked"
			map.data = [ method: "command" ]
			break
		case 4: // Unlocked by command
			map.descriptionText = "Unlocked"
			map.data = [ method: "command" ]
			break
		case 5: // Locked with keypad
			if (cmd.eventParameter || cmd.alarmLevel) {
				codeID = readCodeSlotId(cmd)
				codeName = getCodeName(lockCodes, codeID)
				map.descriptionText = "Locked by \"$codeName\""
				map.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ]
			} else {
				map.descriptionText = "Locked manually"
				map.data = [ method: "keypad" ]
			}
			break
		case 6: // Unlocked with keypad
			if (cmd.eventParameter || cmd.alarmLevel) {
				codeID = readCodeSlotId(cmd)
				codeName = getCodeName(lockCodes, codeID)
				map.descriptionText = "Unlocked by \"$codeName\""
				map.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ]
			}
			break
		case 7:
			map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
			map.data = [ method: "manual" ]
			break
		case 8:
			map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
			map.data = [ method: "command" ]
			break
		case 9: // Auto locked
			map = [ name: "lock", value: "locked", data: [ method: "auto" ] ]
			map.descriptionText = "Auto locked"
			break
		case 0xA:
			map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
			map.data = [ method: "auto" ]
			break
		case 0xB:
			map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
			break
		case 0xC: // All user codes deleted
			result = allCodesDeletedEvent()
			map = [ name: "codeChanged", value: "all deleted", descriptionText: "Deleted all user codes", isStateChange: true ]
			map.data = [notify: true, notificationText: "Deleted all user codes in $deviceName at ${location.name}"]
			result << createEvent(name: "lockCodes", value: (new groovy.json.JsonOutput()).toJson([:]), displayed: false, descriptionText: "'lockCodes' attribute updated")
			break
		case 0xD: // User code deleted
			if (cmd.eventParameter || cmd.alarmLevel) {
				codeID = readCodeSlotId(cmd)
				if (lockCodes[codeID.toString()]) {
					codeName = getCodeName(lockCodes, codeID)
					map = [ name: "codeChanged", value: "$codeID deleted", isStateChange: true ]
					map.descriptionText = "Deleted \"$codeName\""
					map.data = [ codeName: codeName, notify: true, notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ]
					result << codeDeletedEvent(lockCodes, codeID)
				}
			}
			break
		case 0xE: // Master or user code changed/set
			if (cmd.eventParameter || cmd.alarmLevel) {
				codeID = readCodeSlotId(cmd)
				codeName = getCodeNameFromState(lockCodes, codeID)
				changeType = getChangeType(lockCodes, codeID)
				map = [ name: "codeChanged", value: "$codeID $changeType",  descriptionText: "${getStatusForDescription(changeType)} \"$codeName\"", isStateChange: true ]
				map.data = [ codeName: codeName, notify: true, notificationText: "${getStatusForDescription(changeType)} \"$codeName\" in $deviceName at ${location.name}" ]
				if(!isMasterCode(codeID)) {
					result << codeSetEvent(lockCodes, codeID, codeName)
				} else {
					map.descriptionText = "${getStatusForDescription('set')} \"$codeName\""
					map.data.notificationText = "${getStatusForDescription('set')} \"$codeName\" in $deviceName at ${location.name}"
				}
			}
			break
		case 0xF: // Duplicate Pin-code error
			if (cmd.eventParameter || cmd.alarmLevel) {
				codeID = readCodeSlotId(cmd)
				clearStateForSlot(codeID)
				map = [ name: "codeChanged", value: "$codeID failed", descriptionText: "User code is duplicate and not added",
						isStateChange: true, data: [isCodeDuplicate: true] ]
			}
			break
		case 0x10: // Tamper Alarm
		case 0x13:
			map = [ name: "tamper", value: "detected", descriptionText: "Keypad attempts exceed code entry limit", isStateChange: true, displayed: true ]
			break
		case 0x11: // Keypad busy
			map = [ descriptionText: "Keypad is busy" ]
			break
		case 0x12: // Master code changed
			codeName = getCodeNameFromState(lockCodes, 0)
			map = [ name: "codeChanged", value: "0 set", descriptionText: "${getStatusForDescription('set')} \"$codeName\"", isStateChange: true ]
			map.data = [ codeName: codeName, notify: true, notificationText: "${getStatusForDescription('set')} \"$codeName\" in $deviceName at ${location.name}" ]
			break
		case 0xFE:
			// delegating it to handleAlarmReportUsingAlarmType
			return handleAlarmReportUsingAlarmType(cmd)
		default:
			// delegating it to handleAlarmReportUsingAlarmType
			return handleAlarmReportUsingAlarmType(cmd)
	}

	if (map) {
		if (map.data) {
			map.data.lockName = deviceName
		} else {
			map.data = [ lockName: deviceName ]
		}
		result << createEvent(map)
	}
	result = result.flatten()
	result
}

/**
 * Responsible for handling Burglar AlarmReport command
 *
 * @param cmd: The AlarmReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
private def handleBurglarAlarmReport(cmd) {
	log.trace "[DTH] Executing 'handleBurglarAlarmReport' with cmd = $cmd"
	def result = []
	def deviceName = device.displayName

	def map = [ name: "tamper", value: "detected" ]
	map.data = [ lockName: deviceName ]
	switch (cmd.zwaveAlarmEvent) {
		case 0:
			map.value = "clear"
			map.descriptionText = "Tamper alert cleared"
			break
		case 1:
		case 2:
			map.descriptionText = "Intrusion attempt detected"
			break
		case 3:
			map.descriptionText = "Covering removed"
			break
		case 4:
			map.descriptionText = "Invalid code"
			break
		default:
			// delegating it to handleAlarmReportUsingAlarmType
			return handleAlarmReportUsingAlarmType(cmd)
	}

	result << createEvent(map)
	result
}

/**
 * Responsible for handling Battery AlarmReport command
 *
 * @param cmd: The AlarmReport command to be parsed
 *
 * @return The event(s) to be sent out
 */
private def handleBatteryAlarmReport(cmd) {
	log.trace "[DTH] Executing 'handleBatteryAlarmReport' with cmd = $cmd"
	def result = []
	def deviceName = device.displayName
	def map = null
	switch(cmd.zwaveAlarmEvent) {
		case 0x01: //power has been applied, check if the battery level updated
			result << response(secure(zwave.batteryV1.batteryGet()))
			break;
		case 0x0A:
			map = [ name: "battery", value: 1, descriptionText: "Battery level critical", displayed: true, data: [ lockName: deviceName ] ]
			break
		case 0x0B:
			map = [ name: "battery", value: 0, descriptionText: "Battery too low to operate lock", isStateChange: true, displayed: true, data: [ lockName: deviceName ] ]
			break
		default:
			// delegating it to handleAlarmReportUsingAlarmType
			return handleAlarmReportUsingAlarmType(cmd)
	}
	result << createEvent(map)
	result
}

/**
 * Responsible for handling AlarmReport commands which are ignored by Access & Burglar handlers
 *
 * @param cmd: The AlarmReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
private def handleAlarmReportUsingAlarmType(cmd) {
	log.trace "[DTH] Executing 'handleAlarmReportUsingAlarmType' with cmd = $cmd"
	def result = []
	def map = null
	def codeID, lockCodes, codeName
	def deviceName = device.displayName
	lockCodes = loadLockCodes()
	switch(cmd.alarmType) {
		case 9:
		case 17:
			map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
			break
		case 16: // Note: for levers this means it's unlocked, for non-motorized deadbolt, it's just unsecured and might not get unlocked
		case 19: // Unlocked with keypad
			map = [ name: "lock", value: "unlocked" ]
			if (cmd.alarmLevel != null) {
				codeID = readCodeSlotId(cmd)
				codeName = getCodeName(lockCodes, codeID)
				map.isStateChange = true // Non motorized locks, mark state changed since it can be unlocked multiple times
				map.descriptionText = "Unlocked by \"$codeName\""
				map.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ]
			}
			break
		case 18: // Locked with keypad
			codeID = readCodeSlotId(cmd)
			map = [ name: "lock", value: "locked" ]
			codeName = getCodeName(lockCodes, codeID)
			map.descriptionText = "Locked by \"$codeName\""
			map.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ]
			break
		case 21: // Manually locked
			map = [ name: "lock", value: "locked", data: [ method: (cmd.alarmLevel == 2) ? "keypad" : "manual" ] ]
			map.descriptionText = "Locked manually"
			break
		case 22: // Manually unlocked
			map = [ name: "lock", value: "unlocked", data: [ method: "manual" ] ]
			map.descriptionText = "Unlocked manually"
			break
		case 23:
			map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
			map.data = [ method: "command" ]
			break
		case 24: // Locked by command
			map = [ name: "lock", value: "locked", data: [ method: "command" ] ]
			map.descriptionText = "Locked"
			break
		case 25: // Unlocked by command
			map = [ name: "lock", value: "unlocked", data: [ method: "command" ] ]
			map.descriptionText = "Unlocked"
			break
		case 26:
			map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
			map.data = [ method: "auto" ]
			break
		case 27: // Auto locked
			map = [ name: "lock", value: "locked", data: [ method: "auto" ] ]
			map.descriptionText = "Auto locked"
			break
		case 32: // All user codes deleted
			result = allCodesDeletedEvent()
			map = [ name: "codeChanged", value: "all deleted", descriptionText: "Deleted all user codes", isStateChange: true ]
			map.data = [notify: true, notificationText: "Deleted all user codes in $deviceName at ${location.name}"]
			result << createEvent(name: "lockCodes", value: (new groovy.json.JsonOutput()).toJson([:]), displayed: false, descriptionText: "'lockCodes' attribute updated")
			break
		case 33: // User code deleted
			codeID = readCodeSlotId(cmd)
			if (lockCodes[codeID.toString()]) {
				codeName = getCodeName(lockCodes, codeID)
				map = [ name: "codeChanged", value: "$codeID deleted", isStateChange: true ]
				map.descriptionText = "Deleted \"$codeName\""
				map.data = [ codeName: codeName, notify: true, notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ]
				result << codeDeletedEvent(lockCodes, codeID)
			}
			break
		case 38: // Non Access
			map = [ descriptionText: "A Non Access Code was entered at the lock", isStateChange: true ]
			break
		case 13:
		case 112: // Master or user code changed/set
			codeID = readCodeSlotId(cmd)
			codeName = getCodeNameFromState(lockCodes, codeID)
			def changeType = getChangeType(lockCodes, codeID)
			map = [ name: "codeChanged", value: "$codeID $changeType", descriptionText:
					"${getStatusForDescription(changeType)} \"$codeName\"", isStateChange: true ]
			map.data = [ codeName: codeName, notify: true, notificationText: "${getStatusForDescription(changeType)} \"$codeName\" in $deviceName at ${location.name}" ]
			if(!isMasterCode(codeID)) {
				result << codeSetEvent(lockCodes, codeID, codeName)
			} else {
				map.descriptionText = "${getStatusForDescription('set')} \"$codeName\""
				map.data.notificationText = "${getStatusForDescription('set')} \"$codeName\" in $deviceName at ${location.name}"
			}
			break
		case 34:
		case 113: // Duplicate Pin-code error
			codeID = readCodeSlotId(cmd)
			clearStateForSlot(codeID)
			map = [ name: "codeChanged", value: "$codeID failed", descriptionText: "User code is duplicate and not added",
					isStateChange: true, data: [isCodeDuplicate: true] ]
			break
		case 130:  // Batteries replaced
			map = [ descriptionText: "Batteries replaced", isStateChange: true ]
			break
		case 131: // Disabled user entered at keypad
			map = [ descriptionText: "Code ${cmd.alarmLevel} is disabled", isStateChange: false ]
			break
		case 161: // Tamper Alarm
			if (cmd.alarmLevel == 2) {
				map = [ name: "tamper", value: "detected", descriptionText: "Front escutcheon removed", isStateChange: true ]
			} else {
				map = [ name: "tamper", value: "detected", descriptionText: "Keypad attempts exceed code entry limit", isStateChange: true, displayed: true ]
			}
			break
		case 167: // Low Battery Alarm
			if (!state.lastbatt || now() - state.lastbatt > 12*60*60*1000) {
				map = [ descriptionText: "Battery low", isStateChange: true ]
				result << response(secure(zwave.batteryV1.batteryGet()))
			} else {
				map = [ name: "battery", value: device.currentValue("battery"), descriptionText: "Battery low", isStateChange: true ]
			}
			break
		case 168: // Critical Battery Alarms
			map = [ name: "battery", value: 1, descriptionText: "Battery level critical", displayed: true ]
			break
		case 169: // Battery too low to operate
			map = [ name: "battery", value: 0, descriptionText: "Battery too low to operate lock", isStateChange: true, displayed: true ]
			break
		default:
			map = [ displayed: false, descriptionText: "Alarm event ${cmd.alarmType} level ${cmd.alarmLevel}" ]
			break
	}

	if (map) {
		if (map.data) {
			map.data.lockName = deviceName
		} else {
			map.data = [ lockName: deviceName ]
		}
		result << createEvent(map)
	}
	result = result.flatten()
	result
}

/**
 * Responsible for parsing UserCodeReport command
 *
 * @param cmd: The UserCodeReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(UserCodeReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(UserCodeReport)' with userIdentifier: ${cmd.userIdentifier} and status: ${cmd.userIdStatus}"
	def result = []
	// cmd.userIdentifier seems to be an int primitive type
	def codeID = cmd.userIdentifier.toString()
	def lockCodes = loadLockCodes()
	def map = [ name: "codeChanged", isStateChange: true ]
	def deviceName = device.displayName
	def userIdStatus = cmd.userIdStatus
	if (userIdStatus == UserCodeReport.USER_ID_STATUS_OCCUPIED ||
			(userIdStatus == UserCodeReport.USER_ID_STATUS_STATUS_NOT_AVAILABLE && cmd.user)) {

		def codeName = getCodeName(lockCodes, codeID)
		def changeType = getChangeType(lockCodes, codeID)
		if (!lockCodes[codeID]) {
			result << codeSetEvent(lockCodes, codeID, codeName)
		} else {
			map.displayed = false
		}
		map.value = "$codeID $changeType"
		map.descriptionText = "${getStatusForDescription(changeType)} \"$codeName\""
		map.data = [ codeName: codeName, lockName: deviceName ]
	} else {
		// We are using userIdStatus here because codeID = 0 is reported when user tries to set programming code as the user code
		// code is not set
		if (lockCodes[codeID]) {
			def codeName = getCodeName(lockCodes, codeID)
			map.value = "$codeID deleted"
			map.descriptionText = "Deleted \"$codeName\""
			map.data = [ codeName: codeName, lockName: deviceName, notify: true, notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ]
			result << codeDeletedEvent(lockCodes, codeID)
		} else {
			map.value = "$codeID unset"
			map.displayed = false
			map.data = [ lockName: deviceName ]
		}
	}
	clearStateForSlot(codeID)
	result << createEvent(map)
	if (codeID.toInteger() == state.checkCode) {  // reloadAllCodes() was called, keep requesting the codes in order
		if (state.checkCode + 1 > state.codes || state.checkCode >= 8) {
			state.remove("checkCode")  // done
			state["checkCode"] = null
			sendEvent(name: "scanCodes", value: "Complete", descriptionText: "Code scan completed", displayed: false)
		} else {
			state.checkCode = state.checkCode + 1  // get next
			result << response(requestCode(state.checkCode))
		}
	}
	if (codeID.toInteger() == state.pollCode) {
		if (state.pollCode + 1 > state.codes || state.pollCode >= 15) {
			state.remove("pollCode")  // done
			state["pollCode"] = null
		} else {
			state.pollCode = state.pollCode + 1
		}
	}

	result = result.flatten()
	result
}

/**
 * Responsible for parsing UsersNumberReport command
 *
 * @param cmd: The UsersNumberReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(UsersNumberReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(UsersNumberReport)' with cmd = $cmd"
	def result = [createEvent(name: "maxCodes", value: cmd.supportedUsers, displayed: false)]
	state.codes = cmd.supportedUsers
	if (state.checkCode) {
		if (state.checkCode <= cmd.supportedUsers) {
			result << response(requestCode(state.checkCode))
		} else {
			state.remove("checkCode")
			state["checkCode"] = null
		}
	}
	result
}

/**
 * Responsible for parsing AssociationReport command
 *
 * @param cmd: The AssociationReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport)' with cmd = $cmd"
	def result = []
	if (cmd.nodeId.any { it == zwaveHubNodeId }) {
		state.remove("associationQuery")
		state["associationQuery"] = null
		result << createEvent(descriptionText: "Is associated")
		state.assoc = zwaveHubNodeId
		if (cmd.groupingIdentifier == 2) {
			result << response(zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:zwaveHubNodeId))
		}
	} else if (cmd.groupingIdentifier == 1) {
		result << response(secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)))
	} else if (cmd.groupingIdentifier == 2) {
		result << response(zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId))
	}
	result
}

/**
 * Responsible for parsing TimeGet command
 *
 * @param cmd: The TimeGet command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.timev1.TimeGet cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.timev1.TimeGet)' with cmd = $cmd"
	def result = []
	def now = new Date().toCalendar()
	if(location.timeZone) now.timeZone = location.timeZone
	result << createEvent(descriptionText: "Requested time update", displayed: false)
	result << response(secure(zwave.timeV1.timeReport(
			hourLocalTime: now.get(Calendar.HOUR_OF_DAY),
			minuteLocalTime: now.get(Calendar.MINUTE),
			secondLocalTime: now.get(Calendar.SECOND)))
	)
	result
}

/**
 * Responsible for parsing BasicSet command
 *
 * @param cmd: The BasicSet command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet)' with cmd = $cmd"
	// DEPRECATED: The old Schlage locks use group 1 for basic control - we don't want that, so unsubscribe from group 1
	def result = [ createEvent(name: "lock", value: cmd.value ? "unlocked" : "locked") ]
	def cmds = [
			zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:zwaveHubNodeId).format(),
			"delay 1200",
			zwave.associationV1.associationGet(groupingIdentifier:2).format()
	]
	[result, response(cmds)]
}

/**
 * Responsible for parsing BatteryReport command
 *
 * @param cmd: The BatteryReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport)' with cmd = $cmd"
	def map = [ name: "battery", unit: "%" ]
	if (cmd.batteryLevel == 0xFF) {
		map.value = 1
		map.descriptionText = "Has a low battery"
	} else {
		map.value = cmd.batteryLevel
		map.descriptionText = "Battery is at ${cmd.batteryLevel}%"
	}
	state.lastbatt = now()
	createEvent(map)
}

/**
 * Responsible for parsing ManufacturerSpecificReport command
 *
 * @param cmd: The ManufacturerSpecificReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport)' with cmd = $cmd"
	def result = []
	def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
	updateDataValue("MSR", msr)
	result << createEvent(descriptionText: "MSR: $msr", isStateChange: false)
	result
}

/**
 * Responsible for parsing VersionReport command
 *
 * @param cmd: The VersionReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport)' with cmd = $cmd"
	def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}"
	updateDataValue("fw", fw)
	if (getDataValue("MSR") == "003B-6341-5044") {
		updateDataValue("ver", "${cmd.applicationVersion >> 4}.${cmd.applicationVersion & 0xF}")
	}
	def text = "${device.displayName}: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}"
	createEvent(descriptionText: text, isStateChange: false)
}

/**
 * Responsible for parsing ApplicationBusy command
 *
 * @param cmd: The ApplicationBusy command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.applicationstatusv1.ApplicationBusy cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.applicationstatusv1.ApplicationBusy)' with cmd = $cmd"
	def msg = cmd.status == 0 ? "try again later" :
			cmd.status == 1 ? "try again in ${cmd.waitTime} seconds" :
					cmd.status == 2 ? "request queued" : "sorry"
	createEvent(displayed: true, descriptionText: "Is busy, $msg")
}

/**
 * Responsible for parsing ApplicationRejectedRequest command
 *
 * @param cmd: The ApplicationRejectedRequest command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.applicationstatusv1.ApplicationRejectedRequest)' with cmd = $cmd"
	createEvent(displayed: true, descriptionText: "Rejected the last request")
}

def zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport' with cmd = $cmd"    
    
    // is this v1AlarmType = 0x2b  (DoorState/DoorSense)
    if (cmd.v1AlarmType == 43) {
        // what is the state of DoorSense
        log.trace "got v1AlarmType = 43"
            
        switch (cmd.v1AlarmLevel) {
            case 0:  // Door is open
                log.trace "got v1AlarmLevel = 0  (Door Open)"
                sendEvent(name: "DoorSense", value: "open", isStateChange: true)
                break;
                
            case 1:  // Door is closed
                log.trace "got v1AlarmLevel = 1  (Door Closed)"
                sendEvent(name: "DoorSense", value: "closed", isStateChange: true)
                break;
                
            case 2:  // Door Propped (door open for longer than configurable door propped open)
                log.trace "got v1AlarmLevel = 2  (Door Propped)"
                sendEvent(name: "DoorSense", value: "Propped", isStateChange: true)
                break;
                
            default:
                log.trace "NotificationReport Error (Received non supported v1AlarmLevel from lock)"
                break;
        }
    }
    
    createEvent(displayed: false, descriptionText: "$cmd")
}

def zwaveEvent(hubitat.zwave.commands.alarmv1.AlarmReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.alarmv1.AlarmReport cmd)' with cmd = $cmd"    
    createEvent(displayed: false, descriptionText: "$cmd")
}

/**
 * Responsible for parsing zwave command
 *
 * @param cmd: The zwave command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.Command cmd) {
//    log.debug "***********************************************************************************"
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.Command)' with cmd = $cmd"
    createEvent(displayed: false, descriptionText: "$cmd")
}

/**
 * Executes lock and then check command with a delay on a lock
 */
def lockAndCheck(doorLockMode) {

	secureSequence([
			zwave.doorLockV1.doorLockOperationSet(doorLockMode: doorLockMode),
			zwave.doorLockV1.doorLockOperationGet()
	], 4200)
}

/**
 * Executes lock command on a lock
 */
def lock() {
	log.trace "[DTH] Executing lock() for device ${device.displayName}"
	lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_SECURED)
}

/**
 * Executes unlock command on a lock
 */
def unlock() {
	log.trace "[DTH] Executing unlock() for device ${device.displayName}"
	lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED)
}

/**
 * Executes unlock with timeout command on a lock
 */
def unlockWithTimeout() {
	log.trace "[DTH] Executing unlockWithTimeout() for device ${device.displayName}"
	lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED_WITH_TIMEOUT)
}

/**
 * PING is used by Device-Watch in attempt to reach the Device
 */
def ping() {
	log.trace "[DTH] Executing ping() for device ${device.displayName}"
	runIn(30, followupStateCheck)
	secure(zwave.doorLockV1.doorLockOperationGet())
}

/**
 * Checks the door lock state. Also, schedules checking of door lock state every one hour.
 */
def followupStateCheck() {
	runEvery1Hour(stateCheck)
	stateCheck()
}

/**
 * Checks the door lock state
 */
def stateCheck() {
	sendHubCommand(new hubitat.device.HubAction(secure(zwave.doorLockV1.doorLockOperationGet())))
}

/**
 * Called when the user taps on the refresh button
 */
def refresh() {
	log.trace "[DTH] Executing refresh() for device ${device.displayName}"

	def cmds = secureSequence([zwave.doorLockV1.doorLockOperationGet(), zwave.batteryV1.batteryGet()])
	if (!state.associationQuery) {
		cmds << "delay 4200"
		cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format()  // old Schlage locks use group 2 and don't secure the Association CC
		cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
		state.associationQuery = now()
	} else if (now() - state.associationQuery.toLong() > 9000) {
		cmds << "delay 6000"
		cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format()
		cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))
		cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format()
		cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
		state.associationQuery = now()
	}
	state.lastLockDetailsQuery = now()

	cmds
}

/**
 * Called by the Smart Things platform in case Polling capability is added to the device type
 */
def poll() {
	log.trace "[DTH] Executing poll() for device ${device.displayName}"
	def cmds = []
	// Only check lock state if it changed recently or we haven't had an update in an hour
	def latest = device.currentState("lock")?.date?.time
	if (!latest || !secondsPast(latest, 6 * 60) || secondsPast(state.lastPoll, 55 * 60)) {
		cmds << secure(zwave.doorLockV1.doorLockOperationGet())
		state.lastPoll = now()
	} else if (!state.lastbatt || now() - state.lastbatt > 53*60*60*1000) {
		cmds << secure(zwave.batteryV1.batteryGet())
		state.lastbatt = now()  //inside-214
	}
	if (state.assoc != zwaveHubNodeId && secondsPast(state.associationQuery, 19 * 60)) {
		cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format()
		cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))
		cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format()
		cmds << "delay 6000"
		cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
		cmds << "delay 6000"
		state.associationQuery = now()
	} else {
		// Only check lock state once per hour
		if (secondsPast(state.lastPoll, 55 * 60)) {
			cmds << secure(zwave.doorLockV1.doorLockOperationGet())
			state.lastPoll = now()
		} else if (!state.MSR) {
			cmds << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
		} else if (!state.fw) {
			cmds << zwave.versionV1.versionGet().format()
		} else if (!device.currentValue("maxCodes")) {
			state.pollCode = 1
			cmds << secure(zwave.userCodeV1.usersNumberGet())
		} else if (state.pollCode && state.pollCode <= state.codes) {
			cmds << requestCode(state.pollCode)
		} else if (!state.lastbatt || now() - state.lastbatt > 53*60*60*1000) {
			cmds << secure(zwave.batteryV1.batteryGet())
		}
	}

	if (cmds) {
		log.debug "poll is sending ${cmds.inspect()}"
		cmds
	} else {
		// workaround to keep polling from stopping due to lack of activity
		sendEvent(descriptionText: "skipping poll", isStateChange: true, displayed: false)
		null
	}
}

/**
 * Returns the command for user code get
 *
 * @param codeID: The code slot number
 *
 * @return The command for user code get
 */
def requestCode(codeID) {
	secure(zwave.userCodeV1.userCodeGet(userIdentifier: codeID))
}

/**
 * API endpoint for server smart app to populate the attributes. Called only when the attributes are not populated.
 *
 * @return The command(s) fired for reading attributes
 */
def reloadAllCodes() {
	log.trace "[DTH] Executing 'reloadAllCodes()' by ${device.displayName}"
	sendEvent(name: "scanCodes", value: "Scanning", descriptionText: "Code scan in progress", displayed: false)
	def lockCodes = loadLockCodes()
	sendEvent(lockCodesEvent(lockCodes))
	state.checkCode = state.checkCode ?: 1

	def cmds = []
	// Not calling validateAttributes() here because userNumberGet command will be added twice
	if (!state.codes) {
		// BUG: There might be a bug where Schlage does not return the below number of codes
		cmds << secure(zwave.userCodeV1.usersNumberGet())
	} else {
		sendEvent(name: "maxCodes", value: state.codes, displayed: false)
		cmds << requestCode(state.checkCode)
	}
	if(cmds.size() > 1) {
		cmds = delayBetween(cmds, 4200)
	}
	cmds
}

def getCodes() {
    return reloadAllCodes()
}

/**
 * API endpoint for setting the user code length on a lock. This is specific to Schlage locks.
 *
 * @param length: The user code length
 *
 * @returns The command fired for writing the code length attribute
 */
def setCodeLength(length) {
	return null
}

/**
 * API endpoint for setting a user code on a lock
 *
 * @param codeID: The code slot number
 *
 * @param code: The code PIN
 *
 * @param codeName: The name of the code
 *
 * @returns cmds: The commands fired for creation and checking of a lock code
 */
def setCode(codeID, code, codeName = null) {
//    log.debug "code = " + code
//    log.debug "!code = " + !code
	if (!code) {
		log.trace "[DTH] Executing 'nameSlot()' by ${this.device.displayName}"
		nameSlot(codeID, codeName)
		return
	}

	log.trace "[DTH] Executing 'setCode()' by ${this.device.displayName}"
	def strcode = code
	if (code instanceof String) {
		code = code.toList().findResults { if(it > ' ' && it != ',' && it != '-') it.toCharacter() as Short }
	} else {
		strcode = code.collect{ it as Character }.join()
	}

	def strname = (codeName ?: "Code $codeID")
	state["setname$codeID"] = strname

	def cmds = validateAttributes()
	cmds << secure(zwave.userCodeV1.userCodeSet(userIdentifier:codeID, userIdStatus:1, userCode:code))
	if(cmds.size() > 1) {
		cmds = delayBetween(cmds, 4200)
	}
	cmds
}

/**
 * Validates attributes and if attributes are not populated, adds the command maps to list of commands
 * @return List of commands or empty list
 */
def validateAttributes() {
	def cmds = []
	if(!device.currentValue("maxCodes")) {
		cmds << secure(zwave.userCodeV1.usersNumberGet())
	}
	log.trace "validateAttributes returning commands list: " + cmds
	cmds
}

/**
 * API endpoint for setting/deleting multiple user codes on a lock
 *
 * @param codeSettings: The map with code slot numbers and code pins (in case of update)
 *
 * @returns The commands fired for creation and deletion of lock codes
 */
def updateCodes(codeSettings) {
	log.trace "[DTH] Executing updateCodes() for device ${device.displayName}"
	if(codeSettings instanceof String) codeSettings = (new groovy.json.JsonOutput()).parseJson(codeSettings)
	def set_cmds = []
	codeSettings.each { name, updated ->
		if (name.startsWith("code")) {
			def n = name[4..-1].toInteger()
			if (updated && updated.size() >= 4 && updated.size() <= 8) {
				log.debug "Setting code number $n"
				set_cmds << secure(zwave.userCodeV1.userCodeSet(userIdentifier:n, userIdStatus:1, user:updated))
			} else if (updated == null || updated == "" || updated == "0") {
				log.debug "Deleting code number $n"
				set_cmds << deleteCode(n)
			}
		} else log.warn("unexpected entry $name: $updated")
	}
	if (set_cmds) {
		return response(delayBetween(set_cmds, 2200))
	}
	return null
}

/**
 * Renames an existing lock slot
 *
 * @param codeSlot: The code slot number
 *
 * @param codeName The new name of the code
 */
void nameSlot(codeSlot, codeName) {
	codeSlot = codeSlot.toString()
	if (!isCodeSet(codeSlot)) {
		return
	}
	def deviceName = device.displayName
	log.trace "[DTH] - Executing nameSlot() for device $deviceName"
	def lockCodes = loadLockCodes()
	def oldCodeName = getCodeName(lockCodes, codeSlot)
	def newCodeName = codeName ?: "Code $codeSlot"
	lockCodes[codeSlot] = newCodeName
	sendEvent(lockCodesEvent(lockCodes))
	sendEvent(name: "codeChanged", value: "$codeSlot renamed", data: [ lockName: deviceName, notify: false, notificationText: "Renamed \"$oldCodeName\" to \"$newCodeName\" in $deviceName at ${location.name}" ],
			descriptionText: "Renamed \"$oldCodeName\" to \"$newCodeName\"", displayed: true, isStateChange: true)
}

/**
 * API endpoint for deleting a user code on a lock
 *
 * @param codeID: The code slot number
 *
 * @returns cmds: The command fired for deletion of a lock code
 */
def deleteCode(codeID) {
	log.trace "[DTH] Executing 'deleteCode()' by ${this.device.displayName}"
	// AlarmReport when a code is deleted manually on the lock
	secureSequence([
			zwave.userCodeV1.userCodeSet(userIdentifier:codeID, userIdStatus:0),
			zwave.userCodeV1.userCodeGet(userIdentifier:codeID)
	], 4200)
}

/**
 * Encapsulates a command
 *
 * @param cmd: The command to be encapsulated
 *
 * @returns ret: The encapsulated command
 */
private secure(hubitat.zwave.Command cmd) {
//	zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
    zwaveSecureEncap(cmd)    
}

/**
 * Encapsulates list of command and adds a delay
 *
 * @param commands: The list of command to be encapsulated
 *
 * @param delay: The delay between commands
 *
 * @returns The encapsulated commands
 */
private secureSequence(commands, delay=4200) {
	delayBetween(commands.collect{ secure(it) }, delay)
}

/**
 * Checks if the time elapsed from the provided timestamp is greater than the number of senconds provided
 *
 * @param timestamp: The timestamp
 *
 * @param seconds: The number of seconds
 *
 * @returns true if elapsed time is greater than number of seconds provided, else false
 */
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)
}

/**
 * Reads the code name from the 'lockCodes' map
 *
 * @param lockCodes: map with lock code names
 *
 * @param codeID: The code slot number
 *
 * @returns The code name
 */
private String getCodeName(lockCodes, codeID) {
	if (isMasterCode(codeID)) {
		return "Master Code"
	}
	lockCodes[codeID.toString()] ?: "Code $codeID"
}

/**
 * Reads the code name from the device state
 *
 * @param lockCodes: map with lock code names
 *
 * @param codeID: The code slot number
 *
 * @returns The code name
 */
private String getCodeNameFromState(lockCodes, codeID) {
	if (isMasterCode(codeID)) {
		return "Master Code"
	}
	def nameFromLockCodes = lockCodes[codeID.toString()]
	def nameFromState = state["setname$codeID"]
	if(nameFromLockCodes) {
		if(nameFromState) {
			//Updated from smart app
			return nameFromState
		} else {
			//Updated from lock
			return nameFromLockCodes
		}
	} else if(nameFromState) {
		//Set from smart app
		return nameFromState
	}
	//Set from lock
	return "Code $codeID"
}

/**
 * Check if a user code is present in the 'lockCodes' map
 *
 * @param codeID: The code slot number
 *
 * @returns true if code is present, else false
 */
private Boolean isCodeSet(codeID) {
	// BUG: Needed to add loadLockCodes to resolve null pointer when using schlage?
	def lockCodes = loadLockCodes()
	lockCodes[codeID.toString()] ? true : false
}

/**
 * Reads the 'lockCodes' attribute and parses the same
 *
 * @returns Map: The lockCodes map
 */
private Map loadLockCodes() {
    def stuff = device.currentValue("lockCodes")
    def stuff2 = decrypt(stuff)
    //	parseJson(device.currentValue("lockCodes") ?: "{}") ?: [:]
	parseJson(stuff2 ?: "{}") ?: [:]
}

/**
 * Populates the 'lockCodes' attribute by calling create event
 *
 * @param lockCodes The user codes in a lock
 */
private Map lockCodesEvent(lockCodes) {
	createEvent(name: "lockCodes", value: (new groovy.json.JsonOutput()).toJson(lockCodes), displayed: false,
			descriptionText: "'lockCodes' attribute updated")
}

/**
 * Utility function to figure out if code id pertains to master code or not
 *
 * @param codeID - The slot number in which code is set
 * @return - true if slot is for master code, false otherwise
 */
private boolean isMasterCode(codeID) {
	if(codeID instanceof String) {
		codeID = codeID.toInteger()
	}
	(codeID == 0) ? true : false
}

/**
 * Creates the event map for user code creation
 *
 * @param lockCodes: The user codes in a lock
 *
 * @param codeID: The code slot number
 *
 * @param codeName: The name of the user code
 *
 * @return The list of events to be sent out
 */
private def codeSetEvent(lockCodes, codeID, codeName) {
	clearStateForSlot(codeID)
	// codeID seems to be an int primitive type
	lockCodes[codeID.toString()] = (codeName ?: "Code $codeID")
	def result = []
	result << lockCodesEvent(lockCodes)
	def codeReportMap = [ name: "codeReport", value: codeID, data: [ code: "" ], isStateChange: true, displayed: false ]
	codeReportMap.descriptionText = "${device.displayName} code $codeID is set"
	result << createEvent(codeReportMap)
	result
}

/**
 * Creates the event map for user code deletion
 *
 * @param lockCodes: The user codes in a lock
 *
 * @param codeID: The code slot number
 *
 * @return The list of events to be sent out
 */
private def codeDeletedEvent(lockCodes, codeID) {
	lockCodes.remove("$codeID".toString())
	// not sure if the trigger has done this or not
	clearStateForSlot(codeID)
	def result = []
	result << lockCodesEvent(lockCodes)
	def codeReportMap = [ name: "codeReport", value: codeID, data: [ code: "" ], isStateChange: true, displayed: false ]
	codeReportMap.descriptionText = "${device.displayName} code $codeID was deleted"
	result << createEvent(codeReportMap)
	result
}

/**
 * Creates the event map for all user code deletion
 *
 * @return The List of events to be sent out
 */
private def allCodesDeletedEvent() {
	def result = []
	def lockCodes = loadLockCodes()
	def deviceName = device.displayName
	lockCodes.each { id, code ->
		result << createEvent(name: "codeReport", value: id, data: [ code: "" ], descriptionText: "code $id was deleted",
				displayed: false, isStateChange: true)

		def codeName = code
		result << createEvent(name: "codeChanged", value: "$id deleted", data: [ codeName: codeName, lockName: deviceName,
																				 notify: true, notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ],
				descriptionText: "Deleted \"$codeName\"",
				displayed: true, isStateChange: true)
		clearStateForSlot(id)
	}
	result
}

/**
 * Checks if a change type is set or update
 *
 * @param lockCodes: The user codes in a lock
 *
 * @param codeID The code slot number
 *
 * @return "set" or "update" basis the presence of the code id in the lockCodes map
 */
private def getChangeType(lockCodes, codeID) {
	def changeType = "set"
	if (lockCodes[codeID.toString()]) {
		changeType = "changed"
	}
	changeType
}

/**
 * Method to obtain status for descriptuion based on change type
 * @param changeType: Either "set" or "changed"
 * @return "Added" for "set", "Updated" for "changed", "" otherwise
 */
private def getStatusForDescription(changeType) {
	if("set" == changeType) {
		return "Added"
	} else if("changed" == changeType) {
		return "Updated"
	}
	//Don't return null as it cause trouble
	return ""
}

/**
 * Clears the code name and pin from the state basis the code slot number
 *
 * @param codeID: The code slot number
 */
def clearStateForSlot(codeID) {
	state.remove("setname$codeID")
	state["setname$codeID"] = null
}

/**
 * Generic function for reading code Slot ID from AlarmReport command
 * @param cmd: The AlarmReport command
 * @return user code slot id
 */
def readCodeSlotId(hubitat.zwave.commands.alarmv2.AlarmReport cmd) {
	if(cmd.numberOfEventParameters == 1) {
		return cmd.eventParameter[0]
	} else if(cmd.numberOfEventParameters >= 3) {
		return cmd.eventParameter[2]
	}
	return cmd.alarmLevel
}

def logsOff(){
    log.warn "debug logging disabled..."
}

Don't know if you're still trying to get DoorSense working. I got obsessed with it and heavily modified the driver that you linked. It reports door sense events now and registers the door as a contact sensor so existing code can work with it. The lock codes is updated to work with Lock Code Manager as well. I apologize upfront for the coding and commenting quality, especially the commenting. This is just a hobby for me and I've never programmed in an organized setting.

/**
 * 	Z-Wave Lock
 *
 *  Derivative Work Copyright 2025 Joe McNamee
 *  (Previous: Derivative Work Copyright 2019 Hans Andersson, Copyright 2024 William Siggson)
 *
 *  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.
 *
 */

/**
  *  Joe McNamee modified code to work properly with doorsense and enable contact sensor
  *  Also brought event handling to current standard (sendevent vs createevent percolation), corrected lock code support for lock code manager
  *  
  *
  */

metadata {
	definition (name: "Yale Assure Z-Wave Lock", namespace: "Mrjoemac", author: "Joe McNamee") {
        
		capability "Actuator"
		capability "Lock"
		capability "Polling"
		capability "Refresh"
		capability "ContactSensor"
		capability "Lock Codes"
		capability "Battery"
		capability "Health Check"
		capability "Configuration"
		
		command "reloadCodes", [
			[name: "positions", type: "STRING", description: "Comma separated list: individual positions or ranges (e.g. 1,3,5-7). Also ALL for all positions."]
			]

		fingerprint mfr:"0129", prod:"8002", model:"0600", deviceJoinName: "Yale Assure Lock" //YRD416, YRD426, YRD446
		fingerprint mfr:"0129", prod:"8004", model:"0600", deviceJoinName: "Yale Assure Lock Push Button Deadbolt" //YRD216
		fingerprint mfr:"0129", prod:"800B", model:"0F00", deviceJoinName: "Yale Assure Keypad Lever Door Lock" // YRL216-ZW2
		fingerprint mfr:"0129", prod:"800C", model:"0F00", deviceJoinName: "Yale Assure Touchscreen Lever Door Lock" // YRL226-ZW2
		fingerprint mfr:"0129", prod:"8002", model:"1000", deviceJoinName: "Yale Assure Lock" //YRD-ZWM-1
		fingerprint mfr:"0129", prod:"8107", model:"1000", deviceJoinName: "Yale Assure Lock 2 Touch" //YRD450-F-ZW3-619
	}
}

import hubitat.zwave.commands.doorlockv1.*
import hubitat.zwave.commands.usercodev1.*
    
/**
 * Called on app installed
 */
def installed() {
	// Device-Watch pings if no device events received for 1 hour (checkInterval)
	sendEvent(name: "checkInterval", value: 1 * 60 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"])
	scheduleInstalledCheck()
}

/**
 * Verify that we have actually received the lock's initial states.
 * If not, verify that we have at least requested them or request them,
 * and check again.
 */
def scheduleInstalledCheck() {
	runIn(120, installedCheck)
}

def installedCheck() {
	if (device.currentState("lock") && device.currentState("battery")) {
		unschedule("installedCheck")
	} else {
		// We might have called updated() or configure() at some point but not have received a reply, so don't flood the network
		if (!state.lastLockDetailsQuery || secondsPast(state.lastLockDetailsQuery, 2 * 60)) {
			def actions = updated()

			if (actions) {
				sendHubCommand(actions.toHubAction())
			}
		}

		scheduleInstalledCheck()
	}
}

/**
 * Called on app uninstalled
 */
def uninstalled() {
	def deviceName = device.displayName
	log.trace "[DTH] Executing 'uninstalled()' for device $deviceName"
	sendEvent(name: "lockRemoved", value: device.id, isStateChange: true, displayed: false)
}

/**
 * Executed when the user taps on the 'Done' button on the device settings screen. Sends the values to lock.
 *
 * @return hubAction: The commands to be executed
 */
def updated() {
	// Device-Watch pings if no device events received for 1 hour (checkInterval)
	sendEvent(name: "checkInterval", value: 1 * 60 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"])

	def hubAction = null
	try {
		def cmds = []
		if (!device.currentState("lock") || !device.currentState("battery") || !state.configured) {
			log.debug "Returning commands for lock operation get and battery get"
			if (!state.configured) {
				doConfigure()
			}
			refresh()
			cmds << secure(zwave.userCodeV1.usersNumberGet())
			if (!state.MSR) {
				cmds << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
			}
			if (!state.fw) {
				cmds << zwave.versionV1.versionGet().format()
			}
			executeCommands(delayBetween(cmds, 30*1000))
		}
	} catch (e) {
		log.warn "updated() threw $e"
	}
}

/**
 * Configures the device to settings needed by SmarthThings at device discovery time
 *
 */
def configure() {
	log.trace "[DTH] Executing 'configure()' for device ${device.displayName}"
	doConfigure()
}

/**
 * Returns the list of commands to be executed when the device is being configured/paired
 *
 */
def doConfigure() {
	log.trace "[DTH] Executing 'doConfigure()' for device ${device.displayName}"
	state.configured = true
	def cmds = []
	cmds << secure(zwave.doorLockV1.doorLockOperationGet())
	cmds << secure(zwave.batteryV1.batteryGet())
	cmds = delayBetween(cmds, 30*1000)
	executeCommands(cmds)		// run configuration commands

	state.lastLockDetailsQuery = now()

	log.debug "Do configure returning with commands := $cmds"
}

def zwaveEvent(hubitat.zwave.commands.supervisionv1.SupervisionGet cmd) {
    log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.supervisionv1.SupervisionGet cmd)' with cmd = $cmd"
    hubitat.zwave.Command encapCmd = cmd.encapsulatedCommand(commandClassVersions) // we defined commandClassVersions above
    if (encapCmd) {
        zwaveEvent(encapCmd)
    }
    sendHubCommand(new hubitat.device.HubAction(zwaveSecureEncap(zwave.supervisionV1.supervisionReport(sessionID: cmd.sessionID, reserved: 0, moreStatusUpdates: false, status: 0xFF, duration: 0).format()), hubitat.device.Protocol.ZWAVE))
} 

/**
 * Responsible for parsing incoming device messages to generate events
 *
 * @param description: The incoming description from the device
 *
 * @return result: The list of events to be sent out
 *
 */
def parse(String description) {
	log.trace "[DTH] Executing 'parse(String description)' for device ${device.displayName} with description = $description"

	def result = null
	if (description.startsWith("Err")) {
//        log.debug "********************** Err "
		if (state.sec) {
			sendEvent(descriptionText:description, isStateChange:true, displayed:false)
		} else {
			sendEvent(
					descriptionText: "This lock failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.",
					eventType: "ALERT",
					name: "secureInclusion",
					value: "failed",
					displayed: true,
			)
		}
	} else {
//        log.debug "*** description *** = " + description
		def cmd = zwave.parse(description, [ 0x98: 1, 0x62: 1, 0x63: 1, 0x71: 2, 0x72: 2, 0x80: 1, 0x85: 2, 0x86: 1 ])
//        log.debug "*** cmd *** = " + cmd
		if (cmd) {
			result = zwaveEvent(cmd)
            log.debug "Parsed ${cmd} to ${result.inspect()}"
		}
	}
	log.info "[DTH] parse() - returning result=$result"
	return result
}

/**
 * Responsible for parsing ConfigurationReport command
 *
 * @param cmd: The ConfigurationReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd)' with cmd = $cmd"
	return null
}

/**
 * Responsible for parsing SecurityMessageEncapsulation command
 *
 * @param cmd: The SecurityMessageEncapsulation command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation)' with cmd = $cmd"
	def encapsulatedCommand = cmd.encapsulatedCommand([0x62: 1, 0x71: 2, 0x80: 1, 0x85: 2, 0x63: 1, 0x98: 1, 0x86: 1])
	if (encapsulatedCommand) {
		zwaveEvent(encapsulatedCommand)
	}
}

/**
 * Responsible for parsing NetworkKeyVerify command
 *
 * @param cmd: The NetworkKeyVerify command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.securityv1.NetworkKeyVerify cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.securityv1.NetworkKeyVerify)' with cmd = $cmd"
	sendEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful", isStateChange: true)
}

/**
 * Responsible for parsing SecurityCommandsSupportedReport command
 *
 * @param cmd: The SecurityCommandsSupportedReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.securityv1.SecurityCommandsSupportedReport)' with cmd = $cmd"
	state.sec = cmd.commandClassSupport.collect { String.format("%02X ", it) }.join()
	if (cmd.commandClassControl) {
		state.secCon = cmd.commandClassControl.collect { String.format("%02X ", it) }.join()
	}
	sendEvent(name:"secureInclusion", value:"success", descriptionText:"Lock is securely included", isStateChange: true)
}

/**
 * Responsible for parsing DoorLockOperationReport command
 *
 * @param cmd: The DoorLockOperationReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(DoorLockOperationReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(DoorLockOperationReport)' with cmd = $cmd"

	unschedule("followupStateCheck")
	unschedule("stateCheck")

	// DoorLockOperationReport is called when trying to read the lock state or when the lock is locked/unlocked from the DTH or the smart app
//log.debug "****** cmd ****** = " + cmd
	def map = [ name: "lock" ]
	map.data = [ lockName: device.displayName ]
	if (cmd.doorLockMode == 0xFF) {
		map.value = "locked"
		map.descriptionText = "Locked"
	} else if (cmd.doorLockMode >= 0x40) {
		map.value = "unknown"
		map.descriptionText = "Unknown state"
	} else if (cmd.doorLockMode == 0x01) {
		map.value = "unlocked with timeout"
		map.descriptionText = "Unlocked with timeout"
	}  else {
		map.value = "unlocked"
		map.descriptionText = "Unlocked"
		if (state.assoc != zwaveHubNodeId) {
			def cmds = []
			cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))
			cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId)
			cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
			executeCommands(cmds)
		}
	}

	sendEvent(map)
}

def delayLockEvent(data) {
	log.debug "Sending cached lock operation: $data.map"
	sendEvent(data.map)
}

/**
 * Responsible for parsing AlarmReport command
 *
 * @param cmd: The AlarmReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.alarmv2.AlarmReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.alarmv2.AlarmReport)' with cmd = $cmd"

	if (cmd.zwaveAlarmType == 6) {
		handleAccessAlarmReport(cmd)
	} else if (cmd.zwaveAlarmType == 7) {
		handleBurglarAlarmReport(cmd)
	} else if(cmd.zwaveAlarmType == 8) {
		handleBatteryAlarmReport(cmd)
	} else {
		handleAlarmReportUsingAlarmType(cmd)
	}

	log.debug "[DTH] zwaveEvent(hubitat.zwave.commands.alarmv2.AlarmReport) returning with result = $result"
}

/**
 * Responsible for handling Access AlarmReport command
 *
 * @param cmd: The AlarmReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
private def handleAccessAlarmReport(cmd) {
	log.trace "[DTH] Executing 'handleAccessAlarmReport' with cmd = $cmd"
	def result = []
	def map = null
	def codeID, changeType, lockCodes, codeName
	def deviceName = device.displayName
	lockCodes = loadLockCodes()
	if (1 <= cmd.zwaveAlarmEvent && cmd.zwaveAlarmEvent < 10) {
		map = [ name: "lock", value: (cmd.zwaveAlarmEvent & 1) ? "locked" : "unlocked" ]
	}
	switch(cmd.zwaveAlarmEvent) {
		case 1: // Manually locked
			map.descriptionText = "Locked manually"
			map.data = [ method: (cmd.alarmLevel == 2) ? "keypad" : "manual" ]
            sendEvent(name: "lock", value: "locked", descriptionText: "Locked manually", isStateChange: true)
			break
		case 2: // Manually unlocked
			map.descriptionText = "Unlocked manually"
			map.data = [ method: "manual" ]
            sendEvent(name: "lock", value: "unlocked", descriptionText: "Unlocked manually", isStateChange: true)
			break
		case 3: // Locked by command
			map.descriptionText = "Locked"
			map.data = [ method: "command" ]
            sendEvent(name: "lock", value: "locked", descriptionText: "Locked by command", isStateChange: true)
			break
		case 4: // Unlocked by command
			map.descriptionText = "Unlocked"
			map.data = [ method: "command" ]
            sendEvent(name: "lock", value: "unlocked", descriptionText: "Unlocked by command", isStateChange: true)
			break
		case 5: // Locked with keypad
			if (cmd.eventParameter || cmd.alarmLevel) {
				codeID = readCodeSlotId(cmd)
				codeName = getCodeName(lockCodes, codeID)
				map.descriptionText = "$deviceName locked by \"$codeName\""
				map.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ]
                sendEvent(name: "lock", value: "locked", descriptionText: "Locked by $codeName", isStateChange: true)
			} else {
				map.descriptionText = "Locked manually"
				map.data = [ method: "keypad" ]
                sendEvent(name: "lock", value: "locked", descriptionText: "Locked by keypad", isStateChange: true)
			}
			break
		case 6: // Unlocked with keypad
			if (cmd.eventParameter || cmd.alarmLevel) {
				codeID = readCodeSlotId(cmd)
				codeName = getCodeName(lockCodes, codeID)
				map.descriptionText = "Unlocked by \"$codeName\""
				map.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ]
                sendEvent(name: "lock", value: "unlocked", descriptionText: "Unlocked by $codeName", isStateChange: true)
			}
			break
		case 7:
			map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
			map.data = [ method: "manual" ]
			break
		case 8:
			map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
			map.data = [ method: "command" ]
			break
		case 9: // Auto locked
			map = [ name: "lock", value: "locked", data: [ method: "auto" ] ]
			map.descriptionText = "Auto locked"
            sendEvent(name: "lock", value: "locked", descriptionText: "Auto locked", isStateChange: true)
			break    
		case 0xA:
			map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
			map.data = [ method: "auto" ]
			break
		case 0xB:
			map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
			break
		case 0xC: // All user codes deleted
			map = [ name: "codeChanged", value: "all deleted", descriptionText: "Deleted all user codes", isStateChange: true ]
			map.data = [notify: true, notificationText: "Deleted all user codes in $deviceName at ${location.name}"]
			sendLockCodesEvent([:])
			break
		case 0xD: // User code deleted
			if (cmd.eventParameter || cmd.alarmLevel) {
				codePosition = readCodeSlotId(cmd)
				return handleUserCodeDeleted(codePosition)
			}
			break
		case 0xE: // Master or user code changed/set
			if (cmd.eventParameter || cmd.alarmLevel) {
				codePosition = readCodeSlotId(cmd)
				log.debug("code changed from 0xE position $codePosition")
				return handleUserCodeChanged(codePosition)
			}
			break
		case 0xF: // Duplicate Pin-code error
			if (cmd.eventParameter || cmd.alarmLevel) {
				codePosition = readCodeSlotId(cmd)
				return handleUserCodeDuplicate(codePosition)
			}
			break
		case 0x10: // Tamper Alarm
		case 0x13:
			map = [ name: "tamper", value: "detected", descriptionText: "Keypad attempts exceed code entry limit", isStateChange: true, displayed: true ]
			break
		case 0x11: // Keypad busy
			map = [ descriptionText: "Keypad is busy" ]
			break
		case 0x12: // Master code changed
			map = [ name: "codeChanged", value: "0 set", descriptionText: "master code changed", isStateChange: true ]
			map.data = [ notify: true, notificationText: "master code changed in $deviceName at ${location.name}" ]
			break
        case 22:
        case 23:
            return handleDoorSenseReport(cmd)
		case 0xFE:		// delegating it to handleAlarmReportUsingAlarmType
		default:
			// delegating it to handleAlarmReportUsingAlarmType
			return handleAlarmReportUsingAlarmType(cmd)
	}

	if (map) {
		if (map.data) {
			map.data.lockName = deviceName
		} else {
			map.data = [ lockName: deviceName ]
		}
		sendEvent(map)
	}
}

/**
 *
 */
private def handleAllUserCodesDeleted() {
	log.trace "[DTH] Executing 'handleAllUserCodesDeleted' for device ${device.displayName}"
}

/**
 *
 */
private def handleUserCodeDeleted(codePosition) {
	log.trace "[DTH] Executing 'handleUserCodeDeleted' for device ${device.displayName}"
	lockCodes = loadLockCodes()
	name = lockCodes[codePosition.toString()].name

	def map = [ name: "codeChanged", value: "$codePosition deleted", descriptionText: "User code for $name at $codePosition deleted", isStateChange: true, data: [isCodeDuplicate: true] ]

	lockCodes.remove(codePosition.toString())
	sendLockCodesEvent(lockCodes)
	sendEvent(map)
}

/**
 *
 */
private def handleUserCodeChanged(codePosition) {
	log.trace "[DTH] Executing 'handleUserCodeChanged' for device ${device.displayName}"
	lockCodes = loadLockCodes()
	log.debug("codes: $lockCodes")
	log.debug("codePosition: $codePosition")
	name = lockCodes[codePosition.toString()].name

	def map = [ name: "codeChanged", isStateChange: true, value: "$codePosition changed", descriptionText: "Code for $name at position $codePosition changed" ]
	if(!isMasterCode(codeID)) {
		validateLockCode(codePosition)
	} else {
		map.descriptionText = "${getStatusForDescription('set')} \"$codeName\""
		map.data.notificationText = "${getStatusForDescription('set')} \"$codeName\" in $deviceName at ${location.name}"
	}
	sendEvent(map)
}

/**
 *
 */
private def handleUserCodeDuplicate(codePosition) {
	log.trace "[DTH] Executing 'handleUserCodeDuplicate' for device ${device.displayName}"
	def map = [ name: "codeChanged", value: "$codePosition failed, duplicate pin", descriptionText: "User code is duplicate and not added", isStateChange: true, data: [isCodeDuplicate: true] ]
	lockCodes = loadLockCodes()
	def duplicateCode = lockCodes[codePosition.toString()].code
	def positionToDelete = null
	lockCodes.each{ k, v -> if (v.code == duplicateCode) {positionToDelete = k} }

	if (positionToDelete) {
		name = lockCodes[positionToDelete].name
		map.value = "$positionToDelete failed, duplicate pin"
		map.descriptionText = "code for $name at position $positionToDelete failed, duplicate pin"
		lockCodes.remove(positionToDelete)
	}
	sendLockCodesEvent(lockCodes)
	sendEvent(map)
}


/**
 * Responsible for handling DoorSense events
 *
 * @param cmd: The AlarmReport command to be parsed
 *
 * @return: does not return data
 */
private def handleDoorSenseReport(cmd) {
    log.trace "[DTH] Executing 'handleDoorSenseReport' with cmd = $cmd"
	def deviceName = device.displayName
    
    def map = [ name: "contact", value: "open", descriptionText: "", isStateChange: true ]
    map.data = [ lockName: deviceName ]
    
    switch (cmd.alarmLevel) {
        case 0:  // Door is open
            map.value = "open"
            map.descriptionText = "Door opened"
            sendEvent(map)
            break;
        
        case 1:  // Door is closed
            map.value = "closed"
            map.descriptionText = "Door closed"
            sendEvent(map)
            break;

        case 2:  // Door Propped (door open for longer than configurable door propped open)
            map.value = "open"
            map.descriptionText = "Door propped opened"
            sendEvent(map)
            break;

        default:
            log.trace "NotificationReport Error (Received non supported DoorSense alarmLevel from lock)"
            break;
    }
}


/**
 * Responsible for handling Burglar AlarmReport command
 *
 * @param cmd: The AlarmReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
private def handleBurglarAlarmReport(cmd) {
	log.trace "[DTH] Executing 'handleBurglarAlarmReport' with cmd = $cmd"

	def map = [name: "tamper", isStateChange: true, data: [ lockName: device.displayName ] ]
	switch (cmd.zwaveAlarmEvent) {
		case 0:
			map.value = "clear"
			map.descriptionText = "Tamper alert cleared"
			break
		case 1:
		case 2:
			map.value = "detected"
			map.descriptionText = "Intrusion attempt etected"
			break
		case 3:
			map.value = "detected"
			map.descriptionText = "Covering removed"
			break
		case 4:
			map.value = "detected"
			map.descriptionText = "Invalid code"
			break
		default:
			// delegating it to handleAlarmReportUsingAlarmType
			return handleAlarmReportUsingAlarmType(cmd)
	}
	sendEvent(map)
}

/**
 * Responsible for handling Battery AlarmReport command
 *
 * @param cmd: The AlarmReport command to be parsed
 *
 * @return The event(s) to be sent out
 */
private def handleBatteryAlarmReport(cmd) {
	log.trace "[DTH] Executing 'handleBatteryAlarmReport' with cmd = $cmd"
	def deviceName = device.displayName
	switch(cmd.zwaveAlarmEvent) {
		case 0x01: //power has been applied, check if the battery level updated
			executeCommand(secure(zwave.batteryV1.batteryGet()))
			break;
		case 0x0A:
			sendEvent(name: "battery", value: 1, descriptionText: "Battery level critical", displayed: true, data: [ lockName: deviceName ])
			break
		case 0x0B:
			sendEvent(name: "battery", value: 0, descriptionText: "Battery too low to operate lock", isStateChange: true, displayed: true, data: [ lockName: deviceName ])
			break
		default:
			// delegating it to handleAlarmReportUsingAlarmType
			handleAlarmReportUsingAlarmType(cmd)
	}
}

/**
 * Responsible for handling AlarmReport commands which are ignored by Access & Burglar handlers
 *
 * @param cmd: The AlarmReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
private def handleAlarmReportUsingAlarmType(cmd) {
	log.trace "[DTH] Executing 'handleAlarmReportUsingAlarmType' with cmd = $cmd"
	def result = []
	def map = null
	def codeID, lockCodes, codeName
	def deviceName = device.displayName
	lockCodes = loadLockCodes()
	switch(cmd.alarmType) {
		case 9:
		case 17:
			map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
			break
		case 16: // Note: for levers this means it's unlocked, for non-motorized deadbolt, it's just unsecured and might not get unlocked
		case 19: // Unlocked with keypad
			map = [ name: "lock", value: "unlocked" ]
			if (cmd.alarmLevel != null) {
				codeID = readCodeSlotId(cmd)
				codeName = getCodeName(lockCodes, codeID)
				map.isStateChange = true // Non motorized locks, mark state changed since it can be unlocked multiple times
				map.descriptionText = "Unlocked by \"$codeName\""
				map.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ]
			}
			break
		case 18: // Locked with keypad
			codeID = readCodeSlotId(cmd)
			map = [ name: "lock", value: "locked" ]
			codeName = getCodeName(lockCodes, codeID)
			map.descriptionText = "Locked by \"$codeName\""
			map.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ]
			break
		case 21: // Manually locked
			map = [ name: "lock", value: "locked", data: [ method: (cmd.alarmLevel == 2) ? "keypad" : "manual" ] ]
			map.descriptionText = "Locked manually"
			break
		case 22: // Manually unlocked
			map = [ name: "lock", value: "unlocked", data: [ method: "manual" ] ]
			map.descriptionText = "Unlocked manually"
			break
		case 23:
			map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
			map.data = [ method: "command" ]
			break
		case 24: // Locked by command
			map = [ name: "lock", value: "locked", data: [ method: "command" ] ]
			map.descriptionText = "Locked"
			break
		case 25: // Unlocked by command
			map = [ name: "lock", value: "unlocked", data: [ method: "command" ] ]
			map.descriptionText = "Unlocked"
			break
		case 26:
			map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ]
			map.data = [ method: "auto" ]
			break
		case 27: // Auto locked
			map = [ name: "lock", value: "locked", data: [ method: "auto" ] ]
			map.descriptionText = "Auto locked"
			break
		case 32: // All user codes deleted
			map = [ name: "codeChanged", value: "all deleted", descriptionText: "Deleted all user codes", isStateChange: true ]
			map.data = [notify: true, notificationText: "Deleted all user codes in $deviceName at ${location.name}"]
			sendLockCodesEvent([:])
			break
		case 33: // User code deleted
			codePosition = readCodeSlotId(cmd)
			return handleUserCodeDeleted(codePosition)
			break
		case 38: // Non Access
			map = [ descriptionText: "A Non Access Code was entered at the lock", isStateChange: true ]
			break
		case 13:
		case 112: // Master or user code changed/set
			codePosition = readCodeSlotId(cmd)
			log.debug("code changed from 112 position $codePosition")
			return handleUserCodeChanged(codePosition)
			break
		case 34:
		case 113: // Duplicate Pin-code error
			codePosition = readCodeSlotId(cmd)
			return handleDuplicateUserCode(codePosition)
			break
		case 130:  // Batteries replaced
			map = [ descriptionText: "Batteries replaced", isStateChange: true ]
			break
		case 131: // Disabled user entered at keypad
			map = [ descriptionText: "Code ${cmd.alarmLevel} is disabled", isStateChange: false ]
			break
		case 161: // Tamper Alarm
			if (cmd.alarmLevel == 2) {
				map = [ name: "tamper", value: "detected", descriptionText: "Front escutcheon removed", isStateChange: true ]
			} else {
				map = [ name: "tamper", value: "detected", descriptionText: "Keypad attempts exceed code entry limit", isStateChange: true, displayed: true ]
			}
			break
		case 167: // Low Battery Alarm
			if (!state.lastbatt || now() - state.lastbatt > 12*60*60*1000) {
				map = [ descriptionText: "Battery low", isStateChange: true ]
				executeCommand(secure(zwave.batteryV1.batteryGet()))
			} else {
				map = [ name: "battery", value: device.currentValue("battery"), descriptionText: "Battery low", isStateChange: true ]
			}
			break
		case 168: // Critical Battery Alarms
			map = [ name: "battery", value: 1, descriptionText: "Battery level critical", displayed: true ]
			break
		case 169: // Battery too low to operate
			map = [ name: "battery", value: 0, descriptionText: "Battery too low to operate lock", isStateChange: true, displayed: true ]
			break
		default:
			map = [ displayed: false, descriptionText: "Alarm event ${cmd.alarmType} level ${cmd.alarmLevel}" ]
			break
	}

	if (map) {
		if (map.data) {
			map.data.lockName = deviceName
		} else {
			map.data = [ lockName: deviceName ]
		}
		sendEvent(map)
	}
}

/**
 * refactoring
 * Responsible for parsing UserCodeReport command
 *
 * @param cmd: The UserCodeReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(UserCodeReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(UserCodeReport)' with userIdentifier: ${cmd.userIdentifier} and status: ${cmd.userIdStatus} and full cmd $cmd"
	def result = []
	// cmd.userIdentifier seems to be an int primitive type
	def codePosition = cmd.userIdentifier.toString()
	def lockCodes = loadLockCodes()
	def map = [ name: "codeChanged", isStateChange: true ]
	def deviceName = device.displayName
	def userIdStatus = cmd.userIdStatus

	if (userIdStatus == UserCodeReport.USER_ID_STATUS_OCCUPIED ||
			(userIdStatus == UserCodeReport.USER_ID_STATUS_STATUS_NOT_AVAILABLE && cmd.user)) {
		log.trace("User code for position $codePosition is occupied")
		if (!lockCodes.containsKey(codePosition) || lockCodes[codePosition].code != cmd.userCode.toString()) {
			log.trace("User code for position $codePosition is different or non-existent")
			lockCodes[codePosition].code = cmd.userCode.toString()
			map.value = "code updated for $codePosition"
			map.descriptionText = "Code for ${lockCodes[codePosition].name} at position $codePosition updated"
		}
		
		// code is already set
	} else {
		// We are using userIdStatus here because codeID = 0 is reported when user tries to set programming code as the user code
		// code is not set
		log.trace("User code for position $codePosition is not set, removing from lockCodes")
		lockCodes.remove(codePosition)
	}

	sendLockCodesEvent(lockCodes)

	if (state.checkCode == codePosition.toInteger()) {
		reloadList = state.codeReloadList
		if (codePosition.toInteger() == reloadList.first()) {
			reloadList = reloadList.drop(1)
			state.codeReloadList = reloadList
		}
		if (reloadList.size() > 0) {
			state.checkCode = reloadList.first()
			executeCommand(secure(zwave.userCodeV1.userCodeGet(userIdentifier:state.checkCode.toInteger())))
		} else {
			state.checkCode = null
			state.codeReloadList = null
			sendEvent(name: "reloadCodes", value: "reloading complete", descriptionText: "Code reload complete")
			log.trace("Code reload complete")
		}
	}
}

/**
 * Responsible for parsing UsersNumberReport command
 *
 * @param cmd: The UsersNumberReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(UsersNumberReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(UsersNumberReport)' with cmd = $cmd"
	sendEvent(name: "maxCodes", value: cmd.supportedUsers, displayed: false)
}

/**
 * Responsible for parsing AssociationReport command
 *
 * @param cmd: The AssociationReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport)' with cmd = $cmd"
	if (cmd.nodeId.any { it == zwaveHubNodeId }) {
		state.remove("associationQuery")
		state["associationQuery"] = null
		sendEvent(descriptionText: "Is associated")
		state.assoc = zwaveHubNodeId
		if (cmd.groupingIdentifier == 2) {
			executeCommand(secure(zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:zwaveHubNodeId)))
		}
	} else if (cmd.groupingIdentifier == 1) {
		executeCommand(secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)))
	} else if (cmd.groupingIdentifier == 2) {
		executeCommand(secure(zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId)))
	}
}

/**
 * Responsible for parsing TimeGet command
 *
 * @param cmd: The TimeGet command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.timev1.TimeGet cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.timev1.TimeGet)' with cmd = $cmd"
	def result = []
	def now = new Date().toCalendar()
	if(location.timeZone) now.timeZone = location.timeZone
	sendEvent(descriptionText: "Requested time update", displayed: false)
	executeCommand(secure(zwave.timeV1.timeReport(
			hourLocalTime: now.get(Calendar.HOUR_OF_DAY),
			minuteLocalTime: now.get(Calendar.MINUTE),
			secondLocalTime: now.get(Calendar.SECOND)))
	)
}

/**
 * Responsible for parsing BasicSet command
 *
 * @param cmd: The BasicSet command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.basicv1.BasicSet)' with cmd = $cmd"
	// DEPRECATED: The old Schlage locks use group 1 for basic control - we don't want that, so unsubscribe from group 1
	sendEvent(name: "lock", value: cmd.value ? "unlocked" : "locked")
	def cmds = [
			zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:zwaveHubNodeId).format(),
			"delay 1200",
			zwave.associationV1.associationGet(groupingIdentifier:2).format()
	]
	executeCommands(cmds)
}

/**
 * Responsible for parsing BatteryReport command
 *
 * @param cmd: The BatteryReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport)' with cmd = $cmd"
	def map = [ name: "battery", unit: "%" ]
	if (cmd.batteryLevel == 0xFF) {
		map.value = 1
		map.descriptionText = "Has a low battery"
	} else {
		map.value = cmd.batteryLevel
		map.descriptionText = "Battery is at ${cmd.batteryLevel}%"
	}
	state.lastbatt = now()
	sendEvent(map)
}

/**
 * Responsible for parsing ManufacturerSpecificReport command
 *
 * @param cmd: The ManufacturerSpecificReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport)' with cmd = $cmd"
	def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)
	updateDataValue("MSR", msr)
	sendEvent(descriptionText: "MSR: $msr", isStateChange: false)
}

/**
 * Responsible for parsing VersionReport command
 *
 * @param cmd: The VersionReport command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport)' with cmd = $cmd"
	def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}"
	updateDataValue("fw", fw)
	if (getDataValue("MSR") == "003B-6341-5044") {
		updateDataValue("ver", "${cmd.applicationVersion >> 4}.${cmd.applicationVersion & 0xF}")
	}
	def text = "${device.displayName}: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}"
	sendEvent(descriptionText: text, isStateChange: false)
}

/**
 * Responsible for parsing ApplicationBusy command
 *
 * @param cmd: The ApplicationBusy command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.applicationstatusv1.ApplicationBusy cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.applicationstatusv1.ApplicationBusy)' with cmd = $cmd"
	def msg = cmd.status == 0 ? "try again later" :
			cmd.status == 1 ? "try again in ${cmd.waitTime} seconds" :
					cmd.status == 2 ? "request queued" : "sorry"
	sendEvent(displayed: true, descriptionText: "Is busy, $msg")
}

/**
 * Responsible for parsing ApplicationRejectedRequest command
 *
 * @param cmd: The ApplicationRejectedRequest command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.applicationstatusv1.ApplicationRejectedRequest)' with cmd = $cmd"
	sendEvent(displayed: true, descriptionText: "Rejected the last request")
}

def zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport' with cmd = $cmd"    
    
    // is this v1AlarmType = 0x2b  (DoorState/DoorSense)
    if (cmd.v1AlarmType == 43) {
        // what is the state of DoorSense
        log.trace "got v1AlarmType = 43"
            
        switch (cmd.v1AlarmLevel) {
            case 0:  // Door is open
                log.trace "got v1AlarmLevel = 0  (Door Open)"
                sendEvent(name: "DoorSense", value: "open", isStateChange: true)
                break;
                
            case 1:  // Door is closed
                log.trace "got v1AlarmLevel = 1  (Door Closed)"
                sendEvent(name: "DoorSense", value: "closed", isStateChange: true)
                break;
                
            case 2:  // Door Propped (door open for longer than configurable door propped open)
                log.trace "got v1AlarmLevel = 2  (Door Propped)"
                sendEvent(name: "DoorSense", value: "Propped", isStateChange: true)
                break;
                
            default:
                log.trace "NotificationReport Error (Received non supported v1AlarmLevel from lock)"
                break;
        }
    }
}

def zwaveEvent(hubitat.zwave.commands.alarmv1.AlarmReport cmd) {
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.commands.alarmv1.AlarmReport cmd)' with cmd = $cmd"    
    sendEvent(displayed: false, descriptionText: "$cmd")
}

/**
 * Responsible for parsing zwave command
 *
 * @param cmd: The zwave command to be parsed
 *
 * @return The event(s) to be sent out
 *
 */
def zwaveEvent(hubitat.zwave.Command cmd) {
//    log.debug "***********************************************************************************"
	log.trace "[DTH] Executing 'zwaveEvent(hubitat.zwave.Command)' with cmd = $cmd"
    sendEvent(displayed: false, descriptionText: "$cmd")
}

/**
 * Executes lock and then check command with a delay on a lock
 */
def lockAndCheck(doorLockMode) {

	def cmds = secureSequence([
			zwave.doorLockV1.doorLockOperationSet(doorLockMode: doorLockMode),
			zwave.doorLockV1.doorLockOperationGet()
	], 4200)

	executeCommands(cmds)
}

/**
 * Executes lock command on a lock
 */
def lock() {
	log.trace "[DTH] Executing lock() for device ${device.displayName}"
	lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_SECURED)
}

/**
 * Executes unlock command on a lock
 */
def unlock() {
	log.trace "[DTH] Executing unlock() for device ${device.displayName}"
	lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED)
}

/**
 * Executes unlock with timeout command on a lock
 */
def unlockWithTimeout() {
	log.trace "[DTH] Executing unlockWithTimeout() for device ${device.displayName}"
	lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED_WITH_TIMEOUT)
}

/**
 * PING is used by Device-Watch in attempt to reach the Device
 */
def ping() {
	log.trace "[DTH] Executing ping() for device ${device.displayName}"
	runIn(30, followupStateCheck)
	secure(zwave.doorLockV1.doorLockOperationGet())
}

/**
 * Checks the door lock state. Also, schedules checking of door lock state every one hour.
 */
def followupStateCheck() {
	runEvery1Hour(stateCheck)
	stateCheck()
}

/**
 * Checks the door lock state
 */
def stateCheck() {
	sendHubCommand(new hubitat.device.HubAction(secure(zwave.doorLockV1.doorLockOperationGet())))
}

/**
 * Called when the user taps on the refresh button
 */
def refresh() {
	log.trace "[DTH] Executing refresh() for device ${device.displayName}"

	def cmds = secureSequence([zwave.doorLockV1.doorLockOperationGet(), zwave.batteryV1.batteryGet()])
	if (!state.associationQuery) {
		cmds << "delay 4200"
		cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format()  // old Schlage locks use group 2 and don't secure the Association CC
		cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
		state.associationQuery = now()
	} else if (now() - state.associationQuery.toLong() > 9000) {
		cmds << "delay 6000"
		cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format()
		cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))
		cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format()
		cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
		state.associationQuery = now()
	}
	state.lastLockDetailsQuery = now()

	executeCommands(cmds)
}

/**
 * Called by the Smart Things platform in case Polling capability is added to the device type
 */
def poll() {
	log.trace "[DTH] Executing poll() for device ${device.displayName}"
	def cmds = []
	// Only check lock state if it changed recently or we haven't had an update in an hour
	def latest = device.currentState("lock")?.date?.time
	if (!latest || !secondsPast(latest, 6 * 60) || secondsPast(state.lastPoll, 55 * 60)) {
		log.trace "poll is sending doorLockOperationGet"
		cmds << secure(zwave.doorLockV1.doorLockOperationGet())
		state.lastPoll = now()
	} else if (!state.lastbatt || now() - state.lastbatt > 53*60*60*1000) {
		cmds << secure(zwave.batteryV1.batteryGet())
		state.lastbatt = now()  //inside-214
	}
	if (state.assoc != zwaveHubNodeId && secondsPast(state.associationQuery, 19 * 60)) {
		cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format()
		cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))
		cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format()
		cmds << "delay 6000"
		cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1))
		cmds << "delay 6000"
		state.associationQuery = now()
	} else {
		// Only check lock state once per hour
		if (secondsPast(state.lastPoll, 55 * 60)) {
			cmds << secure(zwave.doorLockV1.doorLockOperationGet())
			state.lastPoll = now()
		} else if (!state.MSR) {
			cmds << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format()
		} else if (!state.fw) {
			cmds << zwave.versionV1.versionGet().format()
		} else if (!device.currentValue("maxCodes")) {
			state.pollCode = 1
			cmds << secure(zwave.userCodeV1.usersNumberGet())
		} else if (state.pollCode && state.pollCode <= state.codes) {
			cmds << requestCode(state.pollCode)
		} else if (!state.lastbatt || now() - state.lastbatt > 53*60*60*1000) {
			cmds << secure(zwave.batteryV1.batteryGet())
		}
	}

	if (cmds) {
		log.debug "poll is sending ${cmds.inspect()}"
		executeCommands(cmds)
	} else {
		// workaround to keep polling from stopping due to lack of activity
		sendEvent(descriptionText: "skipping poll", isStateChange: true, displayed: false)
		return null
	}
}

/**
 * Returns the command for user code get
 *
 * @param codeID: The code slot number
 *
 * @return The command for user code get
 */
def requestCode(codeID) {
	executeCommand(secure(zwave.userCodeV1.userCodeGet(userIdentifier: codeID)))
}

/**
 * API endpoint for server smart app to populate the attributes. Called only when the attributes are not populated.
 *
 * @return The command(s) fired for reading attributes
 */
def reloadAllCodes() {
	log.trace "[DTH] Executing 'reloadAllCodes()' by ${device.displayName}"
	sendEvent(name: "scanCodes", value: "Scanning", descriptionText: "Code scan in progress", displayed: false)
	state.checkCode = state.checkCode ?: 1

	def cmds = []
	// Not calling validateAttributes() here because userNumberGet command will be added twice
	if (!state.codes) {
		// BUG: There might be a bug where Schlage does not return the below number of codes
		executeCommand(secure(zwave.userCodeV1.usersNumberGet()))
	} else {
		sendEvent(name: "maxCodes", value: state.codes, displayed: false)
		cmds << requestCode(state.checkCode)
	}
	if(cmds.size() > 1) {
		cmds = delayBetween(cmds, 4200)
	}
	
	executeCommands(cmds)
}

def reloadCodes(positions) {
	log.trace("Executing 'reloadCodes' by ${device.displayName} with $positions")

	if (state.codeReloadList?.size() > 0 || state.checkCode != null) {
		log.warn("Code reload already in progress")
		return
	}

	def positionList = []
	def validPositions = (1..device.currentValue("maxCodes"))

	if (positions == "ALL") {
		positionList = validPositions
	} else {
		def textList = positions.tokenize(",")
		textList.each { v ->
			p = v.trim().tokenize("-")
			if (p.size() == 1) {
				positionList.add(p[0].toInteger())
			} else if (p.size() == 2) {
				(p[0].toInteger()..p[1].toInteger()).each { positionList.add(it.toInteger()) }
			}
		}
		validPositions = validPositions.toSet()
		positionList = positionList.toSet()
		positionList = positionList.intersect(validPositions)
		positionList = positionList.toList().sort()
	}

	if (positionList.size() > 0) {
		sendEvent(name: "reloadCodes", value: "reloading began", descriptionText: "Reloading codes beginning with positions: $positions" )
		state.codeReloadList = (new groovy.json.JsonOutput()).toJson(positionList)
		state.codeReloadList = positionList
		state.checkCode = positionList.first()
		log.trace("Beginning code reload for positions: $positionList")
		executeCommand(secure(zwave.userCodeV1.userCodeGet(userIdentifier:state.checkCode.toInteger())))
	} else {
		log.warn("Invalid code positions: $positions")
	}

}

def getCodes() {
    return loadLockCodes()
}

/**
 * API endpoint for setting the user code length on a lock. This is specific to Schlage locks.
 *
 * @param length: The user code length
 *
 * @returns The command fired for writing the code length attribute
 */
def setCodeLength(length) {
	return null
}

/**
 * API endpoint for setting a user code on a lock
 *
 * @param codeID: The code slot number
 *
 * @param code: The code PIN
 *
 * @param codeName: The name of the code
 *
 * @returns cmds: The commands fired for creation and checking of a lock code
 */
def setCode(codePosition, pin, codeName = null) {
    log.trace("calling set code")
//    log.debug "code = " + code
//    log.debug "!code = " + !code
	if (!pin) {
		log.trace "[DTH] Executing 'nameSlot()' by ${this.device.displayName}"
		// update later Joe
		//nameSlot(codeID, codeName)
		return
	}

	log.trace "[DTH] Executing 'setCode()' by ${this.device.displayName} with codeid $codePosition, code $pin, name $codeName"

	validateAttributes()

	if (addCodeToLockCodes(codePosition, pin, codeName)) {
		executeCommand(secure(zwave.userCodeV1.userCodeSet(userIdentifier:codePosition, userIdStatus:1, userCode:pin)))	
	} else {
		log.warn "[DTH] setCode: Invalid pin: $pin"
	}
}

private def isPinValid(pin) {
	if (pin instanceof List) {
		pin = pin.collect{ it as Character }.join()
	}

	if (pin instanceof String) {
		// trim pin
		pin = pin.trim()
	} else {
		// pin was not a string
		pin = pin.toString()
	}

	if (!(pin =~ /^\d+$/)) {
		// pin was not only numeric digits
		return false
	}

	return pin
}

/**
 */
def boolean addCodeToLockCodes(codePosition, pin, codeName=null, validated=false) {
	lockCodes = loadLockCodes()

	if (!codeName) { codeName = "Code $codePosition" }

	pin = isPinValid(pin)
	if (!pin) { return false }

	lockCodes[codePosition.toString()] = ["name": codeName, "code": pin.toString(), "status": "contingent"]

	sendLockCodesEvent(lockCodes)

	return true
}

def validateLockCode(codePosition, validated=true) {
	lockCades = loadLockCodes()

	lockCodes[codePosition.toString()].status = "active"

	sendLockCodesEvent(lockCodes)
}



/**
 * Validates attributes and if attributes are not populated, adds the command maps to list of commands
 * @return List of commands or empty list
 */
def validateAttributes() {
	if(!device.currentValue("maxCodes")) {
		executeCommand(secure(zwave.userCodeV1.usersNumberGet()))
	}
	log.trace "validateAttributes returning commands list: " + cmds
}

/**
 * API endpoint for deleting a user code on a lock
 *
 * @param codeID: The code slot number
 *
 * @returns cmds: The command fired for deletion of a lock code
 */
def deleteCode(codePosition) {
	log.trace "[DTH] Executing 'deleteCode()' by ${this.device.displayName}"
	// AlarmReport when a code is deleted manually on the lock
	def cmd = secureSequence([
			zwave.userCodeV1.userCodeSet(userIdentifier:codePosition, userIdStatus:0),
			zwave.userCodeV1.userCodeGet(userIdentifier:codePosition)
	], 4200)

	executeCommands(cmd)
}

/**
 * Encapsulates a command
 *
 * @param cmd: The command to be encapsulated
 *
 * @returns ret: The encapsulated command
 */
private secure(hubitat.zwave.Command cmd) {
//	zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
    zwaveSecureEncap(cmd)    
}

/**
 * Encapsulates list of command and adds a delay
 *
 * @param commands: The list of command to be encapsulated
 *
 * @param delay: The delay between commands
 *
 * @returns The encapsulated commands
 */
private secureSequence(commands, delay=4200) {
	delayBetween(commands.collect{ secure(it) }, delay)
}

/**
 * Sends the queued commands
 */
private executeCommands(commands) {
	for (cmd in commands) {
		executeCommand(cmd)
	}
}

/**
 * Sends the command to the hub
 */
private executeCommand(command, callback=null) {
	def hubAction = new hubitat.device.HubAction(command, hubitat.device.Protocol.ZWAVE)
	sendHubCommand(hubAction)
}

/**
 * Checks if the time elapsed from the provided timestamp is greater than the number of senconds provided
 *
 * @param timestamp: The timestamp
 *
 * @param seconds: The number of seconds
 *
 * @returns true if elapsed time is greater than number of seconds provided, else false
 */
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)
}

/**
 * Reads the code name from the 'lockCodes' map
 *
 * @param lockCodes: map with lock code names
 *
 * @param codeID: The code slot number
 *
 * @returns The code name
 */
private String getCodeName(codeID) {
	lockCodes = loadLockCodes()
	if (isMasterCode(codeID)) {
		return "Master Code"
	}
	return lockCodes[codeID.toString()].name
}

/**
 * Check if a user code is present in the 'lockCodes' map
 *
 * @param codeID: The code slot number
 *
 * @returns true if code is present, else false
 */
private Boolean isCodeSet(codeID) {
	// BUG: Needed to add loadLockCodes to resolve null pointer when using schlage?
	def lockCodes = loadLockCodes()
	lockCodes[codeID.toString()] ? true : false
}

/**
 * Reads the 'lockCodes' attribute and parses the same
 *
 * @returns Map: The lockCodes map
 */
private Map loadLockCodes() {
    def stuff = device.currentValue("lockCodes")
    def stuff2 = decrypt(stuff)?:stuff
	parseJson(stuff2 ?: "{}") ?: [:]
}

/**
 * Populates the 'lockCodes' attribute by calling create event
 *
 * @param lockCodes The user codes in a lock
 */
private def sendLockCodesEvent(lockCodes) {
	sendEvent(name: "lockCodes", value: (new groovy.json.JsonOutput()).toJson(lockCodes), displayed: false,
			descriptionText: "'lockCodes' attribute updated")
}

/**
 * Utility function to figure out if code id pertains to master code or not
 *
 * @param codeID - The slot number in which code is set
 * @return - true if slot is for master code, false otherwise
 */
private boolean isMasterCode(codeID) {
	if(codeID instanceof String) {
		codeID = codeID.toInteger()
	}
	(codeID == 0) ? true : false
}

/**
 * Generic function for reading code Slot ID from AlarmReport command
 * @param cmd: The AlarmReport command
 * @return user code slot id
 */
def readCodeSlotId(hubitat.zwave.commands.alarmv2.AlarmReport cmd) {
	if(cmd.numberOfEventParameters == 1) {
		return cmd.eventParameter[0]
	} else if(cmd.numberOfEventParameters >= 3) {
		return cmd.eventParameter[2]
	}
	return cmd.alarmLevel
}

def logsOff(){
    log.warn "debug logging disabled..."
}
3 Likes

Hey there, I appreciate your contribution. I have the Yale Assure Lock 2 with Z-Wave and DoorSense. I already had the lock in Hubitat, but it was giving basic information and nothing about DoorSense. I tried your driver and the device's page didn't change at all. Also, I seem to be unable to stop debug logging. Any ideas what I could be doing wrong?

You may need to hit Configure (assuming there is such a button in the driver) on the lock device page after changing the driver.

I only added to the original driver enough code to make DoorSense an additional attribute to a custom app I wanted to write. The sample app I wrote to test it is as follows:

definition(
    name: "YaleLockTest",
    namespace: "Hubi",
    author: "Hubi",
    description: "Test Out The Door Sense Feature Of The Yale Lock",
    category: "Convenience",
    iconUrl: "",
    iconX2Url: "")

preferences {
    section("Sensors") {
        input name: "Yale", type: "capability.*", title: "Yale"
    }
    section("Logging") {
        input name: "logEnable", type: "bool", title: "Enable logging?"
    }
}

def installed() {
    log.debug "installed()"
}

def updated() {
    log.debug "updated()"
    unsubscribe()
    subscribe(Yale, "DoorSense", YaleHandler)
    subscribe(Yale, "checkInterval", YaleHandler)
    subscribe(Yale, "lockRemoved", YaleHandler)
    subscribe(Yale, "scanCodes", YaleHandler)
    subscribe(Yale, "maxCodes", YaleHandler)
    subscribe(Yale, "codeChanged", YaleHandler)
    subscribe(Yale, "lock", YaleHandler)
    subscribe(Yale, "lockCodes", YaleHandler)
    subscribe(Yale, "lastCodeName", YaleHandler)
    subscribe(Yale, "codeLength", YaleHandler)
    subscribe(Yale, "battery", YaleHandler)
}

def uninstalled() {
    log.debug "uninstalled()"    
}

def YaleHandler(evt) {
    log.debug "***** Entered YaleHandler *****"
        
    log.debug "evt.name = " + evt.name
    log.debug "evt.value = " + evt.value   
    log.debug "evt.date = " + evt.date
}

When I unlock the door, open it, close it, and then lock the door I get the following in the logs:

How did you add the Doorsense kit to the lock. The instructions talk about using the Yale app, but the app seems to require the Wifi/BT module to work. Is there a function on the device page that does the same thing?

All the locks have Bluetooth AFAIK, even if you put the zwave module in. I configured the lock using the Yale app and Bluetooth. I had the lock added as a generic zwave lock for over a year before I started working with this driver. I then changed the driver and modified it until it worked for me. You may have better luck if you configure as a generic zwave lock first and then change the driver to the custom one.

If you haven’t configured door sense using the Yale app you will not get the door position information either. The door sense property is exposed as a contact sensor as that was a built in type that made sense to me. The “contact” property is the door sense reading.

The newer Assure Lock 2 seems to have Bluetooth in it by default. My older Assure 1 seems to need the WiFi module to get Bluetooth. It appears that I need that module in order to use it with the Yale Access app. So I guess I am SOL right now if I want to use Doorsense. I am planning on getting the newer Assure Lock 2 for my backdoor. So I might be able to move things around when I get that lock.

BTW. Thanks for your work in making this.

Ah, that would do it. I only have Assure Lock 2’s and so I only know this works on those locks. I’m sorry I can’t help!

I have a Yale Conexis L1, and I will try this out over the weekend