Adding DoorSense To Z-Wave Yale Driver

I couldn’t get user codes to populate so I just started over.

This is the driver I came up with. It reports lock and unlock locally and through the app, reports who unlocked the door if a pin was used, works with Lock Code Manager, reports DoorSense open/close.

/**
 * 	Yale Assure Lock 2
 *
 *  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.
 *
 *  Features:
 *  - Z-Wave S2 Security (Supervision encapsulation)
 *  - DoorSense (Contact Sensor)
 *  - Lock Code Manager compatibility
 *  - Detailed Event Text Reporting
 */

metadata {
    definition (name: "Yale Assure Lock 2", namespace: "Trunzoc", author: "Trunzoc") {
        capability "Actuator"
        capability "Lock"
        capability "Lock Codes"
        capability "ContactSensor"
        capability "Battery"
        capability "Configuration"
        capability "Refresh"

		fingerprint mfr:"0129", prod:"8104", model:"45D1", deviceJoinName: "Yale Assure Lock 2, YRD410" //YRD410-ZW3
		fingerprint mfr:"0129", prod:"8104", model:"45D2", deviceJoinName: "Yale Assure Lock 2, YRD420" //YRD420-ZW3
		fingerprint mfr:"0129", prod:"8104", model:"45D3", deviceJoinName: "Yale Assure Lock 2, YRD430" //YRD430-ZW3
		fingerprint mfr:"0129", prod:"8104", model:"45D5", deviceJoinName: "Yale Assure Lock 2, YRD450" //YRD450-ZW3
		fingerprint mfr:"0129", prod:"8107", model:"49D1", deviceJoinName: "Yale Assure Lock 2 Touch, YRD410" //YRD410-F-ZW3
		fingerprint mfr:"0129", prod:"8107", model:"49D2", deviceJoinName: "Yale Assure Lock 2 Touch, YRD420" //YRD420-F-ZW3
		fingerprint mfr:"0129", prod:"8107", model:"49D3", deviceJoinName: "Yale Assure Lock 2 Touch, YRD430" //YRD430-F-ZW3
		fingerprint mfr:"0129", prod:"8107", model:"49D5", deviceJoinName: "Yale Assure Lock 2 Touch, YRD450" //YRD450-F-ZW3
    }

    preferences {
        input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true
        input name: "debugEnable", type: "bool", title: "Enable debug logging", defaultValue: true
        input name: "traceEnable", type: "bool", title: "Enable trace logging", defaultValue: false
    }
}

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

// --- Core Z-Wave Parsing ---
def parse(String description) {
	if (traceEnable) log.trace "parse(String description): ${description}"
    def result = null
    def cmd = zwave.parse(description, [ 0x98: 1, 0x62: 1, 0x71: 3, 0x80: 1, 0x85: 2, 0x86: 1, 0x6C: 1 ])
    if (cmd) {
        result = zwaveEvent(cmd)
        if (debugEnable) log.debug "Parsed ${cmd} to ${result.inspect()}"
    }
    return result
}

// --- S2 Security Supervision Handler ---
def zwaveEvent(SupervisionGet cmd) {
	if (traceEnable) log.trace "zwaveEvent(SupervisionGet cmd): ${cmd}"
    if (debugEnable) log.debug "SupervisionGet: $cmd"
    def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions)
    if (encapsulatedCommand) {
        zwaveEvent(encapsulatedCommand)
    }
    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))
    return null
}

def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
	if (traceEnable) log.trace "zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd): ${cmd}"
    def encapsulatedCommand = cmd.encapsulatedCommand([0x62: 1, 0x71: 3, 0x80: 1, 0x85: 2, 0x63: 1, 0x98: 1, 0x86: 1])
    if (encapsulatedCommand) {
        return zwaveEvent(encapsulatedCommand)
    }
}

// --- Lock Operation & State ---
def zwaveEvent(DoorLockOperationReport cmd) {
	if (traceEnable) log.trace "zwaveEvent(DoorLockOperationReport cmd): ${cmd}"
    def map = [name: "lock"]
    if (cmd.doorLockMode == 0xFF) {
        map.value = "locked"
        map.descriptionText = "${device.displayName} was locked"
    } else {
        map.value = "unlocked"
        map.descriptionText = "${device.displayName} was unlocked"
    }
    if (txtEnable) log.info map.descriptionText
    sendEvent(map)
}

// --- Consolidated Event Reporting ---
def zwaveEvent(NotificationReport cmd) {
	if (traceEnable) log.trace "zwaveEvent(NotificationReport cmd): ${cmd}"
  	if (debugEnable) log.debug cmd.event
  
    // Notification Type 0x06: Access Control
    if (cmd.notificationType == 0x06) {
        def map = [name: "lock", isStateChange: true]
        switch (cmd.event) {
            case 0x01: // Manual Lock
                map.value = "locked"
                map.descriptionText = "${device.displayName} was locked manually"
                break
            case 0x02: // Manual Unlock
                map.value = "unlocked"
                map.descriptionText = "${device.displayName} was unlocked manually"
                break
            case 0x03: // RF/App Lock
                map.value = "locked"
                map.descriptionText = "${device.displayName} was locked digitally"
                break
            case 0x04: // RF/App Unlock
                map.value = "unlocked"
                map.descriptionText = "${device.displayName} was unlocked digitally"
                break
            case 0x05: // Keypad Lock
                map.value = "locked"
                map.descriptionText = "${device.displayName} was locked via keypad"
                break
            case 0x06: // Keypad Unlock (With User Data)
                def slotId = cmd.eventParameter[2]
                def codeName = getCodeName(slotId)
                map.value = "unlocked"
                map.descriptionText = "${device.displayName} unlocked by ${codeName}"
				sendEvent(name: "lastCodeName", value: codeName)
				state.remove("lastCodeName")
                break
            case 0x0D: // Code Deleted
            case 0x0E: // Code Added/Changed
                def slotId = cmd.eventParameter[0]
                if (txtEnable) log.info "${device.displayName} syncing code ${slotId}..."
                executeCommand(zwaveSecureEncap(zwave.userCodeV1.userCodeGet(userIdentifier: slotId)))
                return
			case 22: // Door Opened
            	map.name = contact
				map.value = "open"
				map.descriptionText = "${device.displayName} was opened"
				if (txtEnable) log.info "${device.displayName} was opened"
				sendEvent(name: "contact", value: "open", descriptionText: "${device.displayName} was opened")
				break;
            case 23: // Door Closed
            	map.name = contact
				map.value = "closed"
				map.descriptionText = "${device.displayName} was closed"
				if (txtEnable) log.info "${device.displayName} was closed"
				sendEvent(name: "contact", value: "closed", descriptionText: "${device.displayName} was closed")
                break
        }
        if (map.value) sendEvent(map)
    }
}

// Fallback handler for older Yale Z-Wave reporting structures (RF/App/Manual)
def zwaveEvent(hubitat.zwave.commands.alarmv2.AlarmReport cmd) {
	if (traceEnable) log.trace "zwaveEvent(hubitat.zwave.commands.alarmv2.AlarmReport cmd): ${cmd}"
    
    // Alarm Type 0x09: Access Control (Legacy/RF Events)
    if (cmd.alarmType == 0x09) {
        switch (cmd.alarmLevel) {
            case 0x01: // Manual Lock
            case 0x02: // Manual Unlock
            case 0x03: // RF Lock
            case 0x04: // RF Unlock
                def status = (cmd.alarmLevel % 2 != 0) ? "locked" : "unlocked"
                sendEvent(name: "lock", value: status, descriptionText: "${device.displayName} ${status} via ${cmd.alarmLevel > 2 ? 'App' : 'Manual'}")
                break
        }
    }
    // Handle DoorSense (if reported via AlarmReport)
    if (cmd.alarmType == 0x16 || cmd.alarmType == 23) {
        return handleDoorSenseReport(cmd)        
    }
}

// --- Lock Code Manager Integration ---
def zwaveEvent(UserCodeReport cmd) {
	if (traceEnable) log.trace "zwaveEvent(UserCodeReport cmd): ${cmd}"
    def codePosition = cmd.userIdentifier.toString()
    def lockCodes = loadLockCodes()
    
    if (cmd.userIdStatus == UserCodeReport.USER_ID_STATUS_OCCUPIED) {
        def pin = cmd.userCode.toString()
        if (!lockCodes.containsKey(codePosition) || lockCodes[codePosition].code != pin) {
            lockCodes[codePosition] = [name: "User ${codePosition}", code: pin, status: "active"]
            sendEvent(name: "codeChanged", value: "${codePosition} updated", descriptionText: "Code position ${codePosition} updated", isStateChange: true)
        }
    } else {
        if (lockCodes.containsKey(codePosition)) {
            lockCodes.remove(codePosition)
            sendEvent(name: "codeChanged", value: "${codePosition} deleted", descriptionText: "Code position ${codePosition} deleted", isStateChange: true)
        }
    }
    updateLockCodes(lockCodes)
}

def setCode(codePosition, pin, codeName = null) {
	if (traceEnable) log.trace "setCode(codePosition, pin, codeName = null): ${codePosition}, ${pin}, ${codeName}"
    if (!codeName) codeName = "User ${codePosition}"
    def lockCodes = loadLockCodes()
    lockCodes[codePosition.toString()] = [name: codeName, code: pin.toString(), status: "active"]
    updateLockCodes(lockCodes)
    
    executeCommand(zwaveSecureEncap(zwave.userCodeV1.userCodeSet(userIdentifier: codePosition, userIdStatus: 1, userCode: pin)))
}

def deleteCode(codePosition) {
	if (traceEnable) log.trace "deleteCode(codePosition): ${codePosition}"
    executeCommand(zwaveSecureEncap(zwave.userCodeV1.userCodeSet(userIdentifier: codePosition, userIdStatus: 0)))
}

def getCodes() {
	if (traceEnable) log.trace "getCodes()"
    executeCommand(zwaveSecureEncap(zwave.userCodeV1.usersNumberGet()))
}

private getCodeName(slotId) {
	if (traceEnable) log.trace "getCodeName(slotId): ${slotId}"
    def codes = loadLockCodes()
    def entry = codes["${slotId}"] ?: codes[slotId]
    return entry?.name ?: "User ${slotId}"
}

private Map loadLockCodes() {
	if (traceEnable) log.trace "loadLockCodes()"
    def lockCodes = device.currentValue("lockCodes")
    def decrypted = decrypt(lockCodes) ?: lockCodes
    return parseJson(decrypted ?: "{}") ?: [:]
}

private void updateLockCodes(lockCodes) {
	if (traceEnable) log.trace "updateLockCodes(lockCodes): ${lockCodes}"
    def json = new groovy.json.JsonOutput().toJson(lockCodes)
    sendEvent(name: "lockCodes", value: json, displayed: false, descriptionText: "Lock codes updated")
}

def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd) {
	if (traceEnable) log.trace "zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd): ${cmd}"
    // 0xFF (255) is Z-Wave's standard report for a low battery warning
    def percent = (cmd.batteryLevel == 0xFF) ? 1 : cmd.batteryLevel
    
    def descText = "${device.displayName} battery is ${percent}%"
    
    if (txtEnable) log.info descText
    
    sendEvent(
        name: "battery", 
        value: percent, 
        unit: "%", 
        descriptionText: descText,
        isStateChange: true
    )
}

// Catch-all for any other unhandled Z-Wave commands to prevent future MissingMethodExceptions
def zwaveEvent(hubitat.zwave.Command cmd) {
	if (traceEnable) log.trace "zwaveEvent(hubitat.zwave.Command cmd): ${cmd}"
    if (debugEnable) log.debug "Unhandled Z-Wave Command: $cmd"
}

def zwaveEvent(hubitat.zwave.commands.usercodev1.UsersNumberReport cmd) {
	if (traceEnable) log.trace "zwaveEvent(hubitat.zwave.commands.usercodev1.UsersNumberReport cmd): ${cmd}"
    if (debugEnable) log.debug "UsersNumberReport: lock supports ${cmd.supportedUsers} codes"
    
    // Update the maxCodes attribute so Lock Code Manager knows the limit
    sendEvent(name: "maxCodes", value: cmd.supportedUsers, displayed: false)
}

// --- Actuator Commands ---
def lock() {
	if (traceEnable) log.trace "lock()"
    executeCommand(zwaveSecureEncap(zwave.doorLockV1.doorLockOperationSet(doorLockMode: DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_SECURED)))
}

def unlock() {
	if (traceEnable) log.trace "unlock()"
    executeCommand(zwaveSecureEncap(zwave.doorLockV1.doorLockOperationSet(doorLockMode: DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED)))
}

def refresh() {
	if (traceEnable) log.trace "refresh()"
    def cmds = []
    cmds << zwaveSecureEncap(zwave.doorLockV1.doorLockOperationGet())
    cmds << zwaveSecureEncap(zwave.batteryV1.batteryGet())
    sendHubCommand(new hubitat.device.HubMultiAction(cmds, hubitat.device.Protocol.ZWAVE))
}

def configure() {
	if (traceEnable) log.trace "configure()"
    log.info "Configuring Association Groups..."
    // Group 1 is the Lifeline for Z-Wave locks, required to report events to the Hub
    def hubNode = zwaveHubNodeId
    def cmds = []
    cmds << zwaveSecureEncap(zwave.associationV1.associationSet(groupingIdentifier: 1, nodeId: hubNode))
    cmds << zwaveSecureEncap(zwave.associationV1.associationGet(groupingIdentifier: 1))
    
    // Also request a full refresh to align current states
    cmds << zwaveSecureEncap(zwave.doorLockV1.doorLockOperationGet())
    
    sendHubCommand(new hubitat.device.HubMultiAction(cmds, hubitat.device.Protocol.ZWAVE))
}

private executeCommand(command) {
	if (traceEnable) log.trace "executeCommand(command): ${command}"
    sendHubCommand(new hubitat.device.HubAction(command, hubitat.device.Protocol.ZWAVE))
}


1 Like