/** * * 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. * * Modified with lock icons and event log */ metadata { definition (name: "Z-Wave Popp Electronic Door Lock Control Module", namespace: "johndoyle", author: "John Doyle") { capability "Actuator" capability "Lock" capability "Refresh" capability "Sensor" capability "Battery" capability "Health Check" command "unlockwtimeout" /** * zw:Ls type:4001 mfr:0154 prod:0005 model:0001 ver:1.05 zwv:4.38 lib:03 cc:5E,7A,73,5A,98,86,72 sec:30,71,70,59,85,62 role:05 ff:8300 ui:8300 * Secure Features: * 30: Sensor Binary * 71: Alarm * 70: Configuration * 59: Association Grp Info * 85: Association * 62: Door Lock * Insecure Features: * 5E: * 7A: Firmware Update Md * 73: Powerlevel * 5A: Device Reset Locally * 98: Security * 86: Version * 72: Manufacturer Specific * Other: * Zwaveplus Info * Battery */ fingerprint mfr: "0154", prod: "0005", model: "0001" } tiles(scale: 2) { multiAttributeTile(name:"toggle", type: "generic", width: 6, height: 4){ tileAttribute ("device.lock", key: "PRIMARY_CONTROL") { attributeState "locked", label:'Locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#00A0DC", nextState:"unlocking" attributeState "unlocked", label:'unlocked', action:"lock.lock", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff", nextState:"locking" attributeState "unknown", label:"unknown", action:"lock.lock", icon:"st.locks.lock.unknown", backgroundColor:"#ffffff", nextState:"locking" attributeState "locking", label:'locking', icon:"st.locks.lock.locked", backgroundColor:"#00A0DC" attributeState "unlocking", label:'Unlocked', icon:"st.locks.lock.unlocked", backgroundColor:"#e86d13" } tileAttribute ("device.battery", key: "SECONDARY_CONTROL") { attributeState "battery", label:'Battery: ${currentValue}%', unit:"" } } valueTile("status", "device.lock", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "locked", label:'Locked', icon:"st.locks.lock.locked" state "unlocking", label:'Unlocked', icon:"st.locks.lock.unlocked" } standardTile("refresh", "device.lock", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" } main("status") details(["toggle", "battery", "refresh"]) } preferences { input "lockTimeoutSeconds", "number", title: "Timeout (sec)", description: "Lock Timeout in Seconds", range: "0..59", displayDuringSetup: false, defaultValue: 1, required: false input "lockTimeoutMinutes", "number", title: "Timeout (min)", description: "Lock Timeout in Minutes", range: "0..255", displayDuringSetup: false, defaultValue: 0, required: false } } import physicalgraph.zwave.commands.doorlockv1.* import physicalgraph.zwave.commands.usercodev1.* def updated() { log.debug "Update" // Set the configuration configure() // 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]) } def configure() { def cmds = [] cmds << zwave.doorLockV1.doorLockConfigurationSet(insideDoorHandlesState: 0, lockTimeoutMinutes: lockTimeoutMinutes.toInteger(), lockTimeoutSeconds: lockTimeoutSeconds.toInteger(), operationType: 2, outsideDoorHandlesState: 0).format() delayBetween(cmds, 4200) } def parse(String description) { def result = null if (description.startsWith("Err 106")) { if (state.sec) { result = createEvent(descriptionText:description, 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 if (description == "updated") { return null } else { def cmd = zwave.parse(description, [ 0x98: 1, 0x72: 2, 0x85: 2, 0x86: 1 ]) log.debug "cmd parsed to $cmd" if (cmd) { result = zwaveEvent(cmd) } } log.debug "\"$description\" parsed to ${result.inspect()}" result } def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { def encapsulatedCommand = cmd.encapsulatedCommand([0x62: 1, 0x71: 2, 0x80: 1, 0x85: 2, 0x63: 1, 0x98: 1, 0x86: 1]) // log.debug "encapsulated: $encapsulatedCommand" if (encapsulatedCommand) { zwaveEvent(encapsulatedCommand) } } def zwaveEvent(DoorLockOperationReport cmd) { def result = [] unschedule("followupStateCheck") unschedule("stateCheck") def map = [ name: "lock" ] if (cmd.doorLockMode == 0xFF) { map.value = "locked" map.isStateChange = true map.displayed = true map.descriptionText = "Locked" } else if (cmd.doorLockMode >= 0x40) { map.value = "unknown" } else if (cmd.doorLockMode & 1) { map.value = "unlocked with timeout" map.isStateChange = true } else { map.value = "unlocked" if (state.assoc != zwaveHubNodeId) { log.debug "setting association" 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))) } } result ? [createEvent(map), *result] : createEvent(map) } def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) { log.debug "AssociationReport" def result = [] if (cmd.nodeId.any { it == zwaveHubNodeId }) { state.remove("associationQuery") log.debug "$device.displayName is associated to $zwaveHubNodeId" result << createEvent(descriptionText: "$device.displayName 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 } def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { log.debug "Basic Set ${cmd}" 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)] log.debug "BasicSet parsed to ${result.inspect()}" } // Battery powered devices can be configured to periodically wake up and // check in. They send this command and stay awake long enough to receive // commands, or until they get a WakeUpNoMoreInformation command that // instructs them that there are no more commands to receive and they can // stop listening. def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { log.debug "WakeUpNotification" def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] // Only ask for battery if we haven't had a BatteryReport in a while if (!state.lastbatt || (new Date().time) - state.lastbatt > 24*60*60*1000) { result << response(zwave.batteryV1.batteryGet()) result << response("delay 1200") // leave time for device to respond to batteryGet } result << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) result } def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { log.debug "BatteryReport" def map = [ name: "battery", unit: "%", displayed: true ] if (cmd.batteryLevel == 0xFF) { map.value = 1 map.descriptionText = "$device.displayName has a low battery" } else { map.value = cmd.batteryLevel map.descriptionText = "$device.displayName battery is ${cmd.batteryLevel}%" } state.lastbatt = now() createEvent(map) } def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { log.debug "ManufacturerSpecificReport" def result = [] def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) log.debug "msr: $msr" updateDataValue("MSR", msr) result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) result } def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { log.debug "VersionReport" def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}" updateDataValue("fw", fw) if (state.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) } def zwaveEvent(physicalgraph.zwave.Command cmd) { log.debug "Command" createEvent(displayed: false, descriptionText: "$device.displayName: $cmd") } def lockAndCheck(doorLockMode) { secureSequence([ zwave.doorLockV1.doorLockOperationSet(doorLockMode: doorLockMode), ], 4200) } def lock() { unlockwtimeout() // lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_SECURED) } def unlock() { unlockwtimeout() // lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED) } def unlockwtimeout() { // sendEvent(name: "lock", value: "locked", isStateChange: "true", descriptionText: "Unlocked" ) lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED_WITH_TIMEOUT) } def stateCheck() { log.debug "stateCheck()" sendHubCommand(new physicalgraph.device.HubAction(secure(zwave.doorLockV1.doorLockOperationGet()))) } def refresh() { def cmds = [] if (state.assoc == zwaveHubNodeId) { log.debug "$device.displayName is associated to ${state.assoc}" cmds << secure(zwave.doorLockV1.doorLockOperationGet()) cmds << secure(zwave.batteryV1.batteryGet()) } else if (!state.associationQuery) { log.debug "checking association" 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 (secondsPast(state.associationQuery, 9)) { 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() } log.debug "refresh sending ${cmds.inspect()}" cmds } def poll() { log.debug "poll()" 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 (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 } } private secure(physicalgraph.zwave.Command cmd) { log.debug "CMD: $cmd" zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() } private secureSequence(commands, delay=4200) { delayBetween(commands.collect{ secure(it) }, delay) }