From 4364321d68a6fbe817d27e6d33f71c2caf9d116a Mon Sep 17 00:00:00 2001 From: Erik Thayer Date: Mon, 20 Mar 2017 19:52:01 -0500 Subject: [PATCH] Restore reportAllCodes learn method --- .../zwave-lock-reporting.groovy | 682 ++++++++++++++++++ .../lock-manager.src/lock-manager.groovy | 14 +- .../ethayer/lock-user.src/lock-user.groovy | 18 +- smartapps/ethayer/lock.src/lock.groovy | 100 ++- 4 files changed, 776 insertions(+), 38 deletions(-) create mode 100644 devicetypes/ethayer/zwave-lock-reporting.src/zwave-lock-reporting.groovy diff --git a/devicetypes/ethayer/zwave-lock-reporting.src/zwave-lock-reporting.groovy b/devicetypes/ethayer/zwave-lock-reporting.src/zwave-lock-reporting.groovy new file mode 100644 index 0000000..b71908d --- /dev/null +++ b/devicetypes/ethayer/zwave-lock-reporting.src/zwave-lock-reporting.groovy @@ -0,0 +1,682 @@ +/** + * Copyright 2015 SmartThings + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ +metadata { + definition (name: "Z-Wave Lock Reporting", namespace: "ethayer", author: "SmartThings") { + capability "Actuator" + capability "Lock" + capability "Polling" + capability "Refresh" + capability "Sensor" + capability "Lock Codes" + capability "Battery" + + command "unlockwtimeout" + + fingerprint deviceId: "0x4003", inClusters: "0x98" + fingerprint deviceId: "0x4004", inClusters: "0x98" + } + + simulator { + status "locked": "command: 9881, payload: 00 62 03 FF 00 00 FE FE" + status "unlocked": "command: 9881, payload: 00 62 03 00 00 00 FE FE" + + reply "9881006201FF,delay 4200,9881006202": "command: 9881, payload: 00 62 03 FF 00 00 FE FE" + reply "988100620100,delay 4200,9881006202": "command: 9881, payload: 00 62 03 00 00 00 FE FE" + } + + 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:"#79b821", 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:"#79b821" + attributeState "unlocking", label:'unlocking', icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff" + } + } + standardTile("lock", "device.lock", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'lock', action:"lock.lock", icon:"st.locks.lock.locked", nextState:"locking" + } + standardTile("unlock", "device.lock", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'unlock', action:"lock.unlock", icon:"st.locks.lock.unlocked", nextState:"unlocking" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + standardTile("refresh", "device.lock", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "toggle" + details(["toggle", "lock", "unlock", "battery", "refresh"]) + } +} + +import physicalgraph.zwave.commands.doorlockv1.* +import physicalgraph.zwave.commands.usercodev1.* + +def updated() { + try { + if (!state.init) { + state.init = true + response(secureSequence([zwave.doorLockV1.doorLockOperationGet(), zwave.batteryV1.batteryGet()])) + } + } catch (e) { + log.warn "updated() threw $e" + } +} + +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 ]) + 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(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) { + createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful") +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { + state.sec = cmd.commandClassSupport.collect { String.format("%02X ", it) }.join() + if (cmd.commandClassControl) { + state.secCon = cmd.commandClassControl.collect { String.format("%02X ", it) }.join() + } + log.debug "Security command classes: $state.sec" + createEvent(name:"secureInclusion", value:"success", descriptionText:"Lock is securely included") +} + +def zwaveEvent(DoorLockOperationReport cmd) { + def result = [] + def map = [ name: "lock" ] + if (cmd.doorLockMode == 0xFF) { + map.value = "locked" + } else if (cmd.doorLockMode >= 0x40) { + map.value = "unknown" + } else if (cmd.doorLockMode & 1) { + map.value = "unlocked with timeout" + } 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.alarmv2.AlarmReport cmd) { + def result = [] + def map = null + if (cmd.zwaveAlarmType == 6) { + if (1 <= cmd.zwaveAlarmEvent && cmd.zwaveAlarmEvent < 10) { + map = [ name: "lock", value: (cmd.zwaveAlarmEvent & 1) ? "locked" : "unlocked" ] + } + switch(cmd.zwaveAlarmEvent) { + case 1: + map.descriptionText = "$device.displayName was manually locked" + break + case 2: + map.descriptionText = "$device.displayName was manually unlocked" + break + case 5: + if (cmd.eventParameter) { + map.descriptionText = "$device.displayName was locked with code ${cmd.eventParameter.first()}" + map.data = [ usedCode: cmd.eventParameter[0] ] + } + break + case 6: + if (cmd.eventParameter) { + map.descriptionText = "$device.displayName was unlocked with code ${cmd.eventParameter.first()}" + map.data = [ usedCode: cmd.eventParameter[0] ] + } + break + case 9: + map.descriptionText = "$device.displayName was autolocked" + break + case 7: + case 8: + case 0xA: + map = [ name: "lock", value: "unknown", descriptionText: "$device.displayName was not locked fully" ] + break + case 0xB: + map = [ name: "lock", value: "unknown", descriptionText: "$device.displayName is jammed" ] + break + case 0xC: + map = [ name: "codeChanged", value: "all", descriptionText: "$device.displayName: all user codes deleted", isStateChange: true ] + allCodesDeleted() + break + case 0xD: + if (cmd.eventParameter) { + map = [ name: "codeReport", value: cmd.eventParameter[0], data: [ code: "" ], isStateChange: true ] + map.descriptionText = "$device.displayName code ${map.value} was deleted" + map.isStateChange = (state["code$map.value"] != "") + state["code$map.value"] = "" + } else { + map = [ name: "codeChanged", descriptionText: "$device.displayName: user code deleted", isStateChange: true ] + } + break + case 0xE: + map = [ name: "codeChanged", value: cmd.alarmLevel, descriptionText: "$device.displayName: user code added", isStateChange: true ] + if (cmd.eventParameter) { + map.value = cmd.eventParameter[0] + result << response(requestCode(cmd.eventParameter[0])) + } + break + case 0xF: + map = [ name: "codeChanged", descriptionText: "$device.displayName: user code not added, duplicate", isStateChange: true ] + break + case 0x10: + map = [ name: "tamper", value: "detected", descriptionText: "$device.displayName: keypad temporarily disabled", displayed: true ] + break + case 0x11: + map = [ descriptionText: "$device.displayName: keypad is busy" ] + break + case 0x12: + map = [ name: "codeChanged", descriptionText: "$device.displayName: program code changed", isStateChange: true ] + break + case 0x13: + map = [ name: "tamper", value: "detected", descriptionText: "$device.displayName: code entry attempt limit exceeded", displayed: true ] + break + default: + map = map ?: [ descriptionText: "$device.displayName: alarm event $cmd.zwaveAlarmEvent", displayed: false ] + break + } + } else if (cmd.zwaveAlarmType == 7) { + map = [ name: "tamper", value: "detected", displayed: true ] + switch (cmd.zwaveAlarmEvent) { + case 0: + map.value = "clear" + map.descriptionText = "$device.displayName: tamper alert cleared" + break + case 1: + case 2: + map.descriptionText = "$device.displayName: intrusion attempt detected" + break + case 3: + map.descriptionText = "$device.displayName: covering removed" + break + case 4: + map.descriptionText = "$device.displayName: invalid code" + break + default: + map.descriptionText = "$device.displayName: tamper alarm $cmd.zwaveAlarmEvent" + break + } + } else switch(cmd.alarmType) { + case 21: // Manually locked + case 18: // Locked with keypad + case 24: // Locked by command (Kwikset 914) + case 27: // Autolocked + map = [ name: "lock", value: "locked" ] + 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: + map = [ name: "lock", value: "unlocked" ] + if (cmd.alarmLevel) { + map.descriptionText = "$device.displayName was unlocked with code $cmd.alarmLevel" + map.data = [ usedCode: cmd.alarmLevel ] + } + break + case 22: + case 25: // Kwikset 914 unlocked by command + map = [ name: "lock", value: "unlocked" ] + break + case 9: + case 17: + case 23: + case 26: + map = [ name: "lock", value: "unknown", descriptionText: "$device.displayName bolt is jammed" ] + break + case 13: + map = [ name: "codeChanged", value: cmd.alarmLevel, descriptionText: "$device.displayName code $cmd.alarmLevel was added", isStateChange: true ] + result << response(requestCode(cmd.alarmLevel)) + break + case 32: + map = [ name: "codeChanged", value: "all", descriptionText: "$device.displayName: all user codes deleted", isStateChange: true ] + allCodesDeleted() + break + case 33: + map = [ name: "codeReport", value: cmd.alarmLevel, data: [ code: "" ], isStateChange: true ] + map.descriptionText = "$device.displayName code $cmd.alarmLevel was deleted" + map.isStateChange = (state["code$cmd.alarmLevel"] != "") + state["code$cmd.alarmLevel"] = "" + break + case 112: + map = [ name: "codeChanged", value: cmd.alarmLevel, descriptionText: "$device.displayName code $cmd.alarmLevel changed", isStateChange: true ] + result << response(requestCode(cmd.alarmLevel)) + break + case 130: // Yale YRD batteries replaced + map = [ descriptionText: "$device.displayName batteries replaced", isStateChange: true ] + break + case 131: + map = [ /*name: "codeChanged", value: cmd.alarmLevel,*/ descriptionText: "$device.displayName code $cmd.alarmLevel is duplicate", isStateChange: false ] + break + case 161: + if (cmd.alarmLevel == 2) { + map = [ descriptionText: "$device.displayName front escutcheon removed", isStateChange: true ] + } else { + map = [ descriptionText: "$device.displayName detected failed user code attempt", isStateChange: true ] + } + break + case 167: + if (!state.lastbatt || now() - state.lastbatt > 12*60*60*1000) { + map = [ descriptionText: "$device.displayName: battery low", isStateChange: true ] + result << response(secure(zwave.batteryV1.batteryGet())) + } else { + map = [ name: "battery", value: device.currentValue("battery"), descriptionText: "$device.displayName: battery low", displayed: true ] + } + break + case 168: + map = [ name: "battery", value: 1, descriptionText: "$device.displayName: battery level critical", displayed: true ] + break + case 169: + map = [ name: "battery", value: 0, descriptionText: "$device.displayName: battery too low to operate lock", isStateChange: true ] + break + default: + map = [ displayed: false, descriptionText: "$device.displayName: alarm event $cmd.alarmType level $cmd.alarmLevel" ] + break + } + result ? [createEvent(map), *result] : createEvent(map) +} + +def zwaveEvent(UserCodeReport cmd) { + def result = [] + def name = "code$cmd.userIdentifier" + def code = cmd.code + def map = [:] + if (cmd.userIdStatus == UserCodeReport.USER_ID_STATUS_OCCUPIED || + (cmd.userIdStatus == UserCodeReport.USER_ID_STATUS_STATUS_NOT_AVAILABLE && cmd.user && code != "**********")) + { + if (code == "**********") { // Schlage locks send us this instead of the real code + state.blankcodes = true + code = state["set$name"] ?: decrypt(state[name]) ?: code + state.remove("set$name".toString()) + } + if (!code && cmd.userIdStatus == 1) { // Schlage touchscreen sends blank code to notify of a changed code + map = [ name: "codeChanged", value: cmd.userIdentifier, displayed: true, isStateChange: true ] + map.descriptionText = "$device.displayName code $cmd.userIdentifier " + (state[name] ? "changed" : "was added") + code = state["set$name"] ?: decrypt(state[name]) ?: "****" + state.remove("set$name".toString()) + } else { + map = [ name: "codeReport", value: cmd.userIdentifier, data: [ code: code ] ] + map.descriptionText = "$device.displayName code $cmd.userIdentifier is set" + map.displayed = (cmd.userIdentifier != state.requestCode && cmd.userIdentifier != state.pollCode) + map.isStateChange = true + } + result << createEvent(map) + } else { + map = [ name: "codeReport", value: cmd.userIdentifier, data: [ code: "" ] ] + if (state.blankcodes && state["reset$name"]) { // we deleted this code so we can tell that our new code gets set + map.descriptionText = "$device.displayName code $cmd.userIdentifier was reset" + map.displayed = map.isStateChange = true + result << createEvent(map) + state["set$name"] = state["reset$name"] + result << response(setCode(cmd.userIdentifier, state["reset$name"])) + state.remove("reset$name".toString()) + } else { + if (state[name]) { + map.descriptionText = "$device.displayName code $cmd.userIdentifier was deleted" + } else { + map.descriptionText = "$device.displayName code $cmd.userIdentifier is not set" + } + map.displayed = (cmd.userIdentifier != state.requestCode && cmd.userIdentifier != state.pollCode) + map.isStateChange = true + result << createEvent(map) + } + code = "" + } + state[name] = code ? encrypt(code) : code + + if (cmd.userIdentifier == state.requestCode) { // reloadCodes() was called, keep requesting the codes in order + if (state.requestCode + 1 > state.codes || state.requestCode >= 30) { + state.remove("requestCode") // done + } else { + state.requestCode = state.requestCode + 1 // get next + result << response(requestCode(state.requestCode)) + } + } + if (cmd.userIdentifier == state.pollCode) { + if (state.pollCode + 1 > state.codes || state.pollCode >= 30) { + state.remove("pollCode") // done + } else { + state.pollCode = state.pollCode + 1 + } + } + log.debug "code report parsed to ${result.inspect()}" + result +} + +def zwaveEvent(UsersNumberReport cmd) { + def result = [] + state.codes = cmd.supportedUsers + if (state.requestCode && state.requestCode <= cmd.supportedUsers) { + result << response(requestCode(state.requestCode)) + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) { + 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.timev1.TimeGet cmd) { + def result = [] + def now = new Date().toCalendar() + if(location.timeZone) now.timeZone = location.timeZone + result << createEvent(descriptionText: "$device.displayName 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 +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + // 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") ] + result << response(zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:zwaveHubNodeId)) + if (state.assoc != zwaveHubNodeId) { + result << response(zwave.associationV1.associationGet(groupingIdentifier:2)) + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "$device.displayName has a low battery" + } else { + map.value = cmd.batteryLevel + } + state.lastbatt = now() + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + 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) { + 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.commands.applicationstatusv1.ApplicationBusy 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: "$device.displayName is busy, $msg") +} + +def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) { + createEvent(displayed: true, descriptionText: "$device.displayName rejected the last request") +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + createEvent(displayed: false, descriptionText: "$device.displayName: $cmd") +} + +def lockAndCheck(doorLockMode) { + secureSequence([ + zwave.doorLockV1.doorLockOperationSet(doorLockMode: doorLockMode), + zwave.doorLockV1.doorLockOperationGet() + ], 4200) +} + +def lock() { + lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_SECURED) +} + +def unlock() { + lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED) +} + +def unlockwtimeout() { + lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED_WITH_TIMEOUT) +} + +def refresh() { + def cmds = [secure(zwave.doorLockV1.doorLockOperationGet())] + if (state.assoc == zwaveHubNodeId) { + log.debug "$device.displayName is associated to ${state.assoc}" + } 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() { + 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 + } + reportAllCodes(state) +} + +def requestCode(codeNumber) { + secure(zwave.userCodeV1.userCodeGet(userIdentifier: codeNumber)) +} + +def reloadAllCodes() { + def cmds = [] + if (!state.codes) { + state.requestCode = 1 + cmds << secure(zwave.userCodeV1.usersNumberGet()) + } else { + if(!state.requestCode) state.requestCode = 1 + cmds << requestCode(codeNumber) + } + cmds +} + +def setCode(codeNumber, code) { + def strcode = code + log.debug "setting code $codeNumber to $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() + } + if (state.blankcodes) { + // Can't just set, we won't be able to tell if it was successful + if (state["code$codeNumber"] != "") { + if (state["setcode$codeNumber"] != strcode) { + state["resetcode$codeNumber"] = strcode + return deleteCode(codeNumber) + } + } else { + state["setcode$codeNumber"] = strcode + } + } + secureSequence([ + zwave.userCodeV1.userCodeSet(userIdentifier:codeNumber, userIdStatus:1, user:code), + zwave.userCodeV1.userCodeGet(userIdentifier:codeNumber) + ], 7000) +} + +def deleteCode(codeNumber) { + log.debug "deleting code $codeNumber" + secureSequence([ + zwave.userCodeV1.userCodeSet(userIdentifier:codeNumber, userIdStatus:0), + zwave.userCodeV1.userCodeGet(userIdentifier:codeNumber) + ], 7000) +} + +def updateCodes(codeSettings) { + if(codeSettings instanceof String) codeSettings = util.parseJson(codeSettings) + def set_cmds = [] + def get_cmds = [] + codeSettings.each { name, updated -> + def current = decrypt(state[name]) + if (name.startsWith("code")) { + def n = name[4..-1].toInteger() + log.debug "$name was $current, set to $updated" + if (updated?.size() >= 4 && updated != current) { + def cmds = setCode(n, updated) + set_cmds << cmds.first() + get_cmds << cmds.last() + } else if ((current && updated == "") || updated == "0") { + def cmds = deleteCode(n) + set_cmds << cmds.first() + get_cmds << cmds.last() + } else if (updated && updated.size() < 4) { + // Entered code was too short + codeSettings["code$n"] = current + } + } else log.warn("unexpected entry $name: $updated") + } + if (set_cmds) { + return response(delayBetween(set_cmds, 2200) + ["delay 2200"] + delayBetween(get_cmds, 4200)) + } +} + +def getCode(codeNumber) { + decrypt(state["code$codeNumber"]) +} + +def getAllCodes() { + state.findAll { it.key.startsWith 'code' }.collectEntries { + [it.key, (it.value instanceof String && it.value.startsWith("~")) ? decrypt(it.value) : it.value] + } +} + +private secure(physicalgraph.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private secureSequence(commands, delay=4200) { + delayBetween(commands.collect{ secure(it) }, delay) +} + +private Boolean secondsPast(timestamp, seconds) { + if (!(timestamp instanceof Number)) { + if (timestamp instanceof Date) { + timestamp = timestamp.time + } else if ((timestamp instanceof String) && timestamp.isNumber()) { + timestamp = timestamp.toLong() + } else { + return true + } + } + return (now() - timestamp) > (seconds * 1000) +} + +private allCodesDeleted() { + if (state.codes instanceof Integer) { + (1..state.codes).each { n -> + if (state["code$n"]) { + result << createEvent(name: "codeReport", value: n, data: [ code: "" ], descriptionText: "code $n was deleted", + displayed: false, isStateChange: true) + } + state["code$n"] = "" + } + } +} + +def reportAllCodes(state) { + def map = [ name: "reportAllCodes", data: [:], displayed: false, isStateChange: false, type: "physical" ] + state.each { entry -> + //iterate through all the state entries and add them to the event data to be handled by application event handlers + if ( entry.key ==~ /^code\d{1,}/ && entry.value.startsWith("~") ) { + map.data.put(entry.key, decrypt(entry.value)) + } else { + map.data.put(entry.key, entry.value) + } + } + sendEvent(map) +} diff --git a/smartapps/ethayer/lock-manager.src/lock-manager.groovy b/smartapps/ethayer/lock-manager.src/lock-manager.groovy index 0596aac..041f0d6 100755 --- a/smartapps/ethayer/lock-manager.src/lock-manager.groovy +++ b/smartapps/ethayer/lock-manager.src/lock-manager.groovy @@ -57,15 +57,11 @@ def lockInfoPage(params) { section("${lockApp.label}") { def complete = lockApp.isCodeComplete() def refreshComplete = lockApp.isRefreshComplete() - if (lockApp.isInLockErrorMode()) { - paragraph 'Lock info was unable to load. There may be an issue with the lock. Please check batteries or connection and try to refresh lock data again.' - } else { - if (!complete) { - paragraph 'App is learning codes. They will appear here when received.' - } - if (!refreshComplete) { - paragraph 'App is in refresh mode.' - } + if (!complete) { + paragraph 'App is learning codes. They will appear here when received.\n Lock may require special DTH to work properly' + } + if (!refreshComplete) { + paragraph 'App is in refresh mode.' } def codeData = lockApp.codeData() if (codeData) { diff --git a/smartapps/ethayer/lock-user.src/lock-user.groovy b/smartapps/ethayer/lock-user.src/lock-user.groovy index 6c8064f..97f2aaa 100755 --- a/smartapps/ethayer/lock-user.src/lock-user.groovy +++ b/smartapps/ethayer/lock-user.src/lock-user.groovy @@ -51,6 +51,13 @@ def initialize() { subscribeToSchedule() } +def uninstalled() { + unschedule() + + // prompt locks to delete this user + inilializeLocks() +} + def subscribeToSchedule() { if (startTime) { // sechedule time of start! @@ -94,6 +101,7 @@ def calendarEnd() { } def initializeLockData() { + debugger('Initialize lock data for user.') def lockApps = parent.getLockApps() lockApps.each { lockApp -> def lockId = lockApp.lock.id @@ -106,9 +114,10 @@ def initializeLockData() { } def inilializeLocks() { + debugger('User asking for lock init') def lockApps = parent.getLockApps() lockApps.each { lockApp -> - lockApp.setupLockData() + lockApp.queSetupLockData() } } @@ -925,3 +934,10 @@ def sendAskAlexa(message) { descriptionText: message, unit: "User//${userName}") } + +def debugger(message) { + def doDebugger = parent.debuggerOn() + if (doDebugger) { + log.debug(message) + } +} diff --git a/smartapps/ethayer/lock.src/lock.groovy b/smartapps/ethayer/lock.src/lock.groovy index 4dfadc3..31d2910 100644 --- a/smartapps/ethayer/lock.src/lock.groovy +++ b/smartapps/ethayer/lock.src/lock.groovy @@ -39,6 +39,7 @@ def initialize() { unsubscribe() unschedule() subscribe(lock, 'codeReport', updateCode, [filterEvents:false]) + subscribe(lock, "reportAllCodes", pollCodeReport, [filterEvents:false]) subscribe(lock, "lock", codeUsed) setupLockData() } @@ -82,25 +83,21 @@ def setupPage() { def mainPage() { dynamicPage(name: "mainPage", title: "Lock Settings", install: true, uninstall: true) { section("Settings") { - if (state.infoErrorMode) { - paragraph 'Lock info was unable to load. There may be an issue with the lock. Please check batteries or connection and try to refresh lock data again.' - } def actions = location.helloHome?.getPhrases()*.label href(name: 'toNotificationPage', page: 'notificationPage', title: 'Notification Settings', image: 'https://dl.dropboxusercontent.com/u/54190708/LockManager/bullhorn.png') if (actions) { href(name: 'toHelloHomePage', page: 'helloHomePage', title: 'Hello Home Settings', image: 'https://dl.dropboxusercontent.com/u/54190708/LockManager/home.png') } - if (isInit() || state.infoErrorMode) { - href(name: 'toInfoRefreshPage', page: 'infoRefreshPage', title: 'Refresh Lock Data', description: 'Tap to refresh', image: 'https://dl.dropboxusercontent.com/u/54190708/LockManager/refresh.png') - debugger("info mode: ${state.infoErrorMode}") - } else { - paragraph 'Lock is loading data' - } } section('Setup', hideable: true, hidden: true) { label title: 'Label', defaultValue: "Lock: ${lock.label}", required: true, description: 'recommended to start with Lock:' input(name: 'lock', title: 'Which Lock?', type: 'capability.lock', multiple: false, required: true) input(name: 'contactSensor', title: 'Which contact sensor?', type: "capability.contactSensor", multiple: false, required: false) + if (isInit()) { + href(name: 'toInfoRefreshPage', page: 'infoRefreshPage', title: 'Refresh Lock Data', description: 'Tap to request code refresh. Not avalible on all locks.', image: 'https://dl.dropboxusercontent.com/u/54190708/LockManager/refresh.png') + } else { + paragraph 'Lock is loading data' + } } } } @@ -121,10 +118,10 @@ def errorPage() { } } def infoRefreshPage() { - dynamicPage(name: 'infoRefreshPage', title: "Lock Info Refresh", nextPage: 'landingPage') { + dynamicPage(name: 'infoRefreshPage', title: 'Lock Info Refresh', nextPage: 'landingPage') { refreshMode() - section("Ok!") { - paragraph 'Lock is now in refresh mode' + section('Refresh Initiated') { + paragraph 'Lock is now in refresh mode.' } } } @@ -196,18 +193,23 @@ def helloHomePage() { } def refreshMode() { - def codeSlots = 30 + def codeSlots = lockCodeSlots() (1..codeSlots).each { slot -> state.codes["slot${slot}"].codeState = 'refresh' } state.requestCount = 0 state.refreshComplete = false - state.infoErrorMode = false makeRequest() } +def queSetupLockData() { + runIn(10, setupLockData) +} + def setupLockData() { debugger('run lock data setup') + // get report from lock -> reportAllCodes() + def lockUsers = parent.getUserApps() lockUsers.each { lockUser -> // initialize data attributes for this lock. @@ -229,9 +231,12 @@ def setupLockData() { state.pinLength = lock.latestValue('pinLength') } } - def codeSlots = 30 + def codeSlots = lockCodeSlots() + def needPoll = false (1..codeSlots).each { slot -> if (state.codes["slot${slot}"] == null) { + needPoll = true + state.initializeComplete = false state.codes["slot${slot}"] = [:] state.codes["slot${slot}"].slot = slot @@ -240,12 +245,18 @@ def setupLockData() { state.codes["slot${slot}"].codeState = 'unknown' } } - makeRequest() + + if (needPoll) { + log.debug('needs poll') + lock.poll() + } + + setCodes() } def makeRequest() { def requestSlot = false - def codeSlots = 30 + def codeSlots = lockCodeSlots() (1..codeSlots).each { slot -> def codeState = state.codes["slot${slot}"]['codeState'] if (codeState != 'known') { @@ -260,14 +271,15 @@ def makeRequest() { lock.requestCode(requestSlot) } else if (!withinAllowed()){ debugger('Codes not retreived in reasonable time') - debugger('Is the lock online?') - state.infoErrorMode = true + debugger('Is the lock requestCode avalible for this lock?') + state.refreshComplete = true + // run a poll and reset everthing + lock.poll() } else { debugger('no request to make') state.requestCount = 0 state.refreshComplete = true state.initializeComplete = true - state.infoErrorMode = false setCodes() } } @@ -277,8 +289,7 @@ def withinAllowed() { } def allowedAttempts() { - // will be code slots x2 - return 30 * 2 + return lockCodeSlots() * 2 } def updateCode(event) { @@ -300,7 +311,7 @@ def updateCode(event) { debugger("Recieved: s:${slot} c:${code}") // check logic to see if all codes are in known state - if (!state.initializeComplete || !state.refreshComplete) { + if (!state.refreshComplete) { runIn(5, makeRequest) } if (previousCode != code) { @@ -309,6 +320,34 @@ def updateCode(event) { } } +def pollCodeReport(evt) { + def codeData = new JsonSlurper().parseText(evt.data) + state.codeSlots = codeData.codes + + def codeSlots = lockCodeSlots() + debugger("Recieved: ${codeData}") + (1..codeSlots).each { slot-> + def code = codeData."code${slot}" + if (code.isNumber()) { + // do nothing, looks good! + } else { + // It's easier on logic if code is empty to be null + code = null + } + + def previousCode = state.codes["slot${slot}"]['code'] + + state.codes["slot${slot}"]['code'] = code + if (!state.refreshComplete) { + // don't change state if in refresh mode + state.codes["slot${slot}"]['codeState'] = 'known' + } + } + state.initializeComplete = true + // Set codes loaded, set new codes. + setCodes() +} + def codeUsed(evt) { def lockId = lock.id def message = '' @@ -417,6 +456,7 @@ def codeUsed(evt) { } def setCodes() { + debugger('run code logic') def codes = state.codes def sortedCodes = codes.sort{it.value.slot} sortedCodes.each { data -> @@ -430,7 +470,7 @@ def setCodes() { // is inactive, should not be set state.codes["slot${data.slot}"].correctValue = null } - } else if (overwriteMode) { + } else if (parent.overwriteMode) { state.codes["slot${data.slot}"].correctValue = null } else { // do nothing! @@ -616,6 +656,14 @@ def isRefreshComplete() { return state.refreshComplete } +def lockCodeSlots() { + def codeSlots = 30 + if (state?.codeSlots?.isNumber()) { + codeSlots = state.codeSlots + } + return codeSlots +} + def codeData() { return state.codes } @@ -633,10 +681,6 @@ def pinLength() { return state.pinLength } -def isInLockErrorMode() { - return state.infoErrorMode -} - def debugger(message) { def doDebugger = parent.debuggerOn() if (doDebugger) {