diff --git a/devicetypes/smartthings/zwave-lock-reporting.src/zwave-lock-reporting.groovy b/devicetypes/smartthings/zwave-lock-reporting.src/zwave-lock-reporting.groovy new file mode 100644 index 0000000..618ded5 --- /dev/null +++ b/devicetypes/smartthings/zwave-lock-reporting.src/zwave-lock-reporting.groovy @@ -0,0 +1,702 @@ +/** + * 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: "smartthings", 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:"#ffa81e", nextState:"locking" + attributeState "unknown", label:"unknown", action:"lock.lock", icon:"st.locks.lock.unknown", backgroundColor:"#ffa81e", nextState:"locking" + attributeState "locking", label:'locking', icon:"st.locks.lock.locked", backgroundColor:"#79b821" + attributeState "unlocking", label:'unlocking', icon:"st.locks.lock.unlocked", backgroundColor:"#ffa81e" + } + } + 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] ] + } + else{ + map.descriptionText = "$device.displayName was locked with keypad" + map.data = [ lockedByKeypad: 1 ] + } + 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) { + //kwikset + case 21: // Manually locked + map = [ name: "lock", value: "locked" ] + if (cmd.alarmLevel == 2) { + map.descriptionText = "$device.displayName was locked with Touch" + map.data = [ lockedByKeypad: 1 ] + } else { + map.descriptionText = "$device.displayName: was locked manually" + } + break + case 18: // Locked with keypad + map = [ name: "lock", value: "locked" ] + map.descriptionText = "$device.displayName: was locked with keypad" + map.data = [ lockedByKeypad: 1 ] + break + case 24: // Locked by command (Kwikset 914) + break; + 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 + break + 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) +} \ No newline at end of file diff --git a/smartapps/slickspaces/slickspaces-lock-manager.src/slickspaces-lock-manager.groovy b/smartapps/slickspaces/slickspaces-lock-manager.src/slickspaces-lock-manager.groovy new file mode 100644 index 0000000..c4612ae --- /dev/null +++ b/smartapps/slickspaces/slickspaces-lock-manager.src/slickspaces-lock-manager.groovy @@ -0,0 +1,181 @@ +/** + * Slickspaces Lock Manager + * + * Copyright 2016 Mathew Hunter + * + * 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. + * + * + * + */ +definition( + name: "Slickspaces Lock Manager", + parent: "Slickspaces:Slickspaces v0.25", + namespace: "Slickspaces", + author: "Mathew Hunter", + description: "Allows integration with Slickspaces rental management suite www.slickspaces.com", + category: "Safety & Security", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", + iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", + oauth: true) + +import groovy.json.JsonSlurper + +preferences { + page(name: "SetupLockManager", Title: "Setup Lock Manager", uninstall: true, install: true){ + section("Access Code Locks") { + input "myLocks","capability.lockCodes", title: "Select Locks for Access Codes", required: true, multiple: true, submitOnChange: true + } + section("Slickspaces Secrets shh"){ + input (name:"slicksecret", type:"password") + } + section{ + href(name: "toLockRoutines", page: "lockRoutinesPage", title: "Lock Action") + } + section("Unlock Action"){ + href(name: "toUnlockRoutines", page: "unlockRoutinesPage", title: "Unlock Action") + } + } + page(name:"lockRoutinesPage", title: "Which Routine Would You Like to Run on Lock?", uninstall:false, install: false) + page(name:"unlockRoutinesPage", title: "Which Routine Would You Like to Run on Lock?", uninstall:false, install: false) +} + +def lockRoutinesPage(){ + dynamicPage(name: "lockRoutinesPage"){ + section("lockAction"){ + def routines = location.getHelloHome()?.getPhrases()*.label + log.debug "routines are ${routines}" + if (routines){ + routines.sort() + input "lockRoutine", "enum", title: "Lock Action", options: routines, multiple: false, required: false, submitOnChange: true, refreshAfterSelection: true + } + } + } + +} + +def unlockRoutinesPage(){ + dynamicPage(name: "unlockRoutinesPage", title: "Which Routine Would You Like to Run on Unlock?"){ + section("unlockAction"){ + def routines = location.getHelloHome()?.getPhrases()*.label + if(routines){ + routines.sort() + input "unlockRoutine", "enum", title: "Unlock Action", options: routines, multiple: false, required: false, submitOnChange: true, refreshAfterSelection: true + } + } + } + +} + + +def getLockUsers(id){ + def lockID = id + def targetLock = myLocks.find{ it.id == lockID } + def lockCodes = [] + return [state.codeData] +} + + +def setLockCode(targetLock, codeID, code){ + //dates are provided in java millisecond time, we need to convert to date for groovy + log.debug "setting lock code for ${targetLock} ${code} ${date}" + def myLock = myLocks.find { it.id == targetLock } + myLock.setCode(codeID,code) + return [codeID:codeID, code:code] +} + +def deleteLockCode(lockID, codeID){ + def targetLock = myLocks.find { it.id == lockID } + targetLock.deleteCode(codeID.toInteger()) + return [codeID:codeID] +} + +def deleteAllLockCodes(lockID){ + def targetLock = myLocks.find { it.id == lockID } + 1.upto(30){ + targetLock.deleteCode(it) + } + return [status:'success'] +} + +def storeUserCodes(evt) { + //this is where I need to modify the codes to be stored in slickspaces db send token with request etc. + def codeData = new JsonSlurper().parseText(evt.data) + log.info "poll triggered with ${codeData}" + state.codeData = [codeData] +} + +def pollLocks(id) { + log.info "polling lock for ${id}" + def targetLock = myLocks.find{ it.id == id } + targetLock.poll() + return [completed: "lock: ${targetLock.id}"] +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + subscribe(myLocks, "reportAllCodes", storeUserCodes, [filterEvents:false]) + subscribe(myLocks, "lock", lockHandler, [filterEvents:false]) + // TODO: subscribe to attributes, devices, locations, etc. +} + +def lockHandler(evt) { +log.debug "lock event fired" + def codeData = new JsonSlurper().parseText(evt.data) + log.debug "${codeData}" + if (codeData.usedCode){ + log.info "${codeData.usedCode}" + //schlagg + if (codeData.microDeviceTile){ + if (codeData.usedCode.isNumber() && codeData.microDeviceTile.icon == "st.locks.lock.locked") { + location.helloHome?.execute(settings.lockRoutine) + } + if (codeData.usedCode.isNumber() && codeData.microDeviceTile.icon == "st.locks.lock.unlocked") { + location.helloHome?.execute(settings.unlockRoutine) + } + + } + log.debug "did I break in my previous if statement?" + //weiser + yale + if (!codeData.microDeviceTile){ + log.debug "microDeviceTile was empty" + if(codeData.usedCode.isNumber()){ + //unlocked + log.debug "running home" + location.helloHome?.execute(settings.unlockRoutine) + } + } + + } + //locking with keypad + if (codeData.lockedByKeypad == 1) { + log.info "lockedByKeypad" + location.helloHome?.execute(settings.lockRoutine) + } + + + +} + + +// TODO: implement event handlers \ No newline at end of file diff --git a/smartapps/slickspaces/slickspaces-v0-25.src/slickspaces-v0-25.groovy b/smartapps/slickspaces/slickspaces-v0-25.src/slickspaces-v0-25.groovy new file mode 100644 index 0000000..cc5261b --- /dev/null +++ b/smartapps/slickspaces/slickspaces-v0-25.src/slickspaces-v0-25.groovy @@ -0,0 +1,773 @@ +/** + * Slickspaces + * + * Copyright 2016 Mathew Hunter + * + * 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. + * v0.19 -- split myPlug and mySwitch to two separate classes + * v0.20 -- added status to the getDevices to simplify the UI for www.slickspaces.com + * v0.21 -- added motion sensor and CO to sensor preference section + * v0.22 -- added routines/current mode + * v0.23 -- added child smartapp - Slickspaces Lock Manager + */ + +definition( + name: "Slickspaces v0.25", + namespace: "Slickspaces", + author: "Mathew Hunter", + description: "Slickspaces Rental Management", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", + iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", + oauth: true) + +preferences { + section("Door Locks") { + input "myLock", "capability.lock", title: "Smart Lock", required: false, multiple: true + + // TODO: put inputs here + } + section("Ecobee Thermostats") { + input "myThermostat", "device.ecobeeThermostat", title: "Smart Thermostat", required: false, multiple: true + } + section("Smart Sensors") { + input "mySensor", "capability.contactSensor", title: "Door/Window Sensors", required: false, multiple: true + input "myMotion", "capability.motionSensor", title: "Motion Sensors", required: false, multiple: true + input "myBattery", "capability.battery", title: "Battery Operated Devices", required: false, multiple: true + input "myCO2", "capability.carbonDioxideMeasurement", title: "CO2 Sensors", required: false, multiple: true + input "mySmoke", "capability.smoke", title: "Smoke Detectors", required: false, multiple: true + input "myTemperature", "capability.temperatureMeasurement", title: "Temperature Sensors", required: false, multiple: true + } + section("Remotes") { + input "myButton", "capability.button", title: "Remote Control", required: false, multiple: true + } + section("Light Dimmers"){ + input "myDimmer", "capability.switchLevel", title: "Smart Dimmers", required: false, multiple: true + } + section("Switches (Lights)"){ + input "mySwitch", "capability.switch", title: "Smart Lights (no dimmer)", required: false, multiple: true + } + section("Switches (Electrical Plugs)"){ + input "myPlug", "capability.switch", title: "Smart Plugs", required: false, multiple: true + } + section("Music Player") { + input "myMusic", "capability.musicPlayer", title: "Music Player", required: false, multiple: true + } + section("Addons") { + app(name: "LockManager", appName: "Slickspaces Lock Manager", namespace: "Slickspaces", title: "Slickspaces Lock Manager", multiple: true) + } + +} + +mappings { + //working on it + path("/refresh-all"){ + action:[ + GET:"getAllDeviceStatus" + ] + } + //TODO + path("/refresh/:deviceType/:id"){ + action:[ + GET:"getDeviceStatus" + ] + } + //done + path("/getdevices"){ + action:[ + GET:"getDevices" + ] + } + path("/getlocks"){ + action:[ + GET:"getLocks" + ] + } + //done + path("/getroutines"){ + action:[ + GET:"getRoutines" + ] + } + path("/executeroutine/:routine"){ + action:[ + GET:"executeRoutine" + ] + } + //lock manager mappings + path("/getlockusers/:id"){ + action:[ + GET:"getLockUsers" + ] + } + path("/setlockcode/:id/:code/:date"){ + action:[ + POST:"setLockCode" + ] + } + path("/deletelockcode/:id/:codeID"){ + action:[ + POST:"deleteLockCode" + ] + } + path("/deletealllockcodes/:id"){ + action:[ + POST:"deleteAllLockCodes" + ] + } + path("/poll/:id"){ + action:[ + GET:"pollLocks" + ] + //end of lock manager maps + path("/verifytoken"){ + action:[ + GET:"verifyToken" + ] + } + path("/gettimezone"){ + action:[ + GET:"getTimezone" + ] + } + path("/:deviceType/:id/:action"){ + action:[ + POST:"startAction" + ] + } + path("/unschedule"){ + action:[ + POST:"startUnschedule" + ] + } + path("/timestamp/:timestamp"){ + action:[ + GET: "timestamp" + ] + } + + + +} + +} + +def timestamp(){ + def timestamp = params.timestamp + def resp = [] + def myResponse = [st_timestampwithoffset:dateTimezoneOffset(timestamp),timestampraw:timestamp, date:new Date()] + resp << myResponse + return resp +} + +def startUnschedule(){ + unschedule() +} + +def getLocks(){ + def deviceAttributes + def resp = [] + if (myLock){ + myLock.each { + deviceAttributes = deviceStatus("lock",it.id) + def myResponse = [device_id:it.id,status:deviceAttributes] + resp << myResponse + } + } + return resp +} + +def verifyToken(){ + return [verifyToken:"Success"] +} + +def deleteLockCodeAt(deleteDate, lockID, codeID){ + log.debug "start of deleteLockCodeAt with vals, applied offset date: ${deleteDate}, ${lockID}, ${codeID}" + def lockMgr = findChildAppByName("Slickspaces Lock Manager") + def endDate = new Date(deleteDate) + log.debug "Setting schedule for code delete on created groovy date ${endDate}, for ${lockID}, ${codeID}" + runOnce(endDate, deleteLockCodeNow, [overwrite: false, data:[lockID:lockID, codeID:codeID]]) +} + +def setLockCodeAt(setDate, lockID, codeID, code){ + log.debug "start of setLockCodeAt with vals, applied offset date: ${deleteDate}, ${lockID}, ${codeID}" + def lockMgr = findChildAppByName("Slickspaces Lock Manager") + def startDate = new Date(setDate) + log.debug "Setting schedule for code set on created groovy date ${startDate}, for ${lockID}, ${codeID}, ${code}" + runOnce(startDate, setLockCodeNow, [overwrite: false,data:[lockID:lockID,codeID:codeID,code:code]]) +} + +def dateTimezoneOffset(longDate){ + longDate = longDate.toLong() - location.timeZone.rawOffset + return longDate +} + +def getLockUsers(){ + def lockMgr = findChildAppByName("Slickspaces Lock Manager") + def lockID = params.id + if (lockMgr){ + return lockMgr.getLockUsers(lockID) + } + return [error:"lock manager not installed"] +} + +def pollLocks() { + log.debug "arrived at pollLocks" + def id = params.id + def lockMgr = findChildAppByName("Slickspaces Lock Manager") + log.debug "id is ${id} and lock manager app is ${lockMgr}" + def result = lockMgr.pollLocks(id) + log.info "result was ${result}" + return result +} + +def setLockCodeNow(data){ + log.info "made it to setLockCodeNow!" + log.info "is there a map? ${data["lockID"]}" + def lockMgr = findChildAppByName("Slickspaces Lock Manager") + if (lockMgr){ + lockMgr.setLockCode(data["lockID"],data["codeID"],data["code"]) + } +} +def deleteLockCodeNow(data){ + log.info "made it to deleteLockCodeNow!" + log.info "is there a delete map? ${data["lockID"]}" + def lockMgr = findChildAppByName("Slickspaces Lock Manager") + if (lockMgr){ + lockMgr.deleteLockCode(data["lockID"],data["codeID"]) + } +} + +def setLockCode(){ + log.info "made it to setLockCode" + def lockMgr = findChildAppByName("Slickspaces Lock Manager") + def arrDateRange = params.date.split('-') + def lockID = params.id + def paramCode = params.code + def arrValues = paramCode.split('-') + def codeID = arrValues[0].toInteger() + def code = arrValues[1] + def date = params.date + def response = [:] + + log.debug "${lockID} ${codeID} ${code} ${date} original dates submitted: ${arrDateRange[0]} ${arrDateRange[1]}" + + if (arrDateRange[0].toLong() > 0){ + log.debug "start date for set lock ${dateTimezoneOffset(arrDateRange[0])}" + def startDate = setLockCodeAt(dateTimezoneOffset(arrDateRange[0]), lockID, codeID, code) + response << [startdate: startDate] + } + if (arrDateRange[1].toLong() > 0) { + log.debug "end date for set lock ${arrDateRange[1]}" + def endDate = deleteLockCodeAt(dateTimezoneOffset(arrDateRange[1]), lockID, codeID) + response << [enddate: endDate] + } + + if(arrDateRange[0].toLong() == 0) { + log.debug "setting lock code immediately" + response << lockMgr.setLockCode(lockID, codeID, code) + } + + return response +} + +def deleteLockCode(){ + log.debug "made it to deleteLockCode" + def lockMgr = findChildAppByName("Slickspaces Lock Manager") + log.debug lockMgr + def lockID = params.id + def codeID = params.codeID.toInteger() + + if (lockMgr){ + return lockMgr.deleteLockCode(lockID, codeID) + } + + return [error:"lock manager not installed"] +} + +def deleteAllLockCodes(){ + def lockID = params.id + log.debug "deleting all lock codes for ${lockID}" + def lockMgr = findChildAppByName("Slickspaces Lock Manager") + if (lockMgr){ + return lockMgr.deleteAllLockCodes(lockID) + } + return [error:"Lock not found"] +} + +//gets the status of all devices, returns to the web requestor the attributes of all devices as JSON (used as part of the web api) +def getAllDeviceStatus(){ + //mySwitch, myDimmer, myThermostat, mySensor, myMusic, myLock + def deviceAttributes = [] + def resp = [] + + if (myThermostat){ + myThermostat.each { + deviceAttributes = deviceStatus("thermostat",it.id) + def myResponse = [device_id:it.id,status:deviceAttributes] + resp << myResponse + } + } + if (mySwitch){ + mySwitch.each { + deviceAttributes = deviceStatus("switch",it.id) + def myResponse = [device_id:it.id,status:deviceAttributes] + resp << myResponse + } + } + if (myPlug){ + myPlug.each { + deviceAttributes = deviceStatus("switch",it.id) + def myResponse = [device_id:it.id,status:deviceAttributes] + resp << myResponse + } + } + if (myDimmer){ + myDimmer.each { + deviceAttributes = deviceStatus("dimmer",it.id) + def myResponse = [device_id:it.id,status:deviceAttributes] + resp << myResponse + } + } + if (mySensor){ + mySensor.each { + deviceAttributes = deviceStatus("sensor",it.id) + def myResponse = [device_id:it.id,status:deviceAttributes] + resp << myResponse + } + } + if (myLock){ + myLock.each { + deviceAttributes = deviceStatus("lock",it.id) + def myResponse = [device_id:it.id,status:deviceAttributes] + resp << myResponse + } + } + return resp +} + +//gets the attributes of a particular device by type and device ID (used as part of the web api after sending an action to verify completion) +def getDeviceStatus() { + def deviceType = params.deviceType + def deviceID = params.id + def deviceAttributes = [] + def resp = [] + log.debug "$deviceID" + deviceAttributes = deviceStatus(deviceType,deviceID) + resp << [completed: deviceAttributes] + return resp +} + +// takes a deviceType and array of device ID's and returns their attributes as result +def deviceStatus(deviceType, deviceID){ + log.debug "$deviceID" + def resp + def myDevice + switch (deviceType){ + case "thermostat": + myDevice = myThermostat.find { it.id == deviceID} + resp = [currentTemperature: myDevice.currentTemperature, + coolingSetpoint: myDevice.currentCoolingSetpoint, + heatingSetpoint: myDevice.currentHeatingSetpoint, + deviceTemperatureUnit: myDevice.currentDeviceTemperatureUnit, + thermostatSetpoint: myDevice.currentThermostatSetPoint, + thermostatMode: myDevice.currentThermostatMode, + thermostatFanMode: myDevice.currentThermostatFanMode, + thermostatStatus: myDevice.currentThermostatStatus, + humidity: myDevice.currentHumidity] + break + case "sensor": + myDevice = mySensor.find { it.id == deviceID} + resp = [contact: myDevice.currentValue("contact")] + break + case "switch": + myDevice = mySwitch.find { it.id == deviceID} + resp = [switch: myDevice.currentValue("switch")] + break + case "plug": + myDevice = myPlug.find { it.id == deviceID} + resp = [plug: myDevice.currentValue("switch")] + break + case "lock": + myDevice = myLock.find { it.id == deviceID} + resp = [lock: myDevice.currentLock] + break + case "music": + myDevice = myMusic.find { it.id == deviceID} + resp = [status: myDevice.currentStatus, + level: myDevice.currentLevel, + trackDescription: myDevice.currentTrackDescription, + trackData: myDevice.currentTrackData, + mute: myDevice.currentMute] + break + case "dimmer": + myDevice = myDimmer.find { it.id == deviceID} + resp = [switch: myDevice.currentSwitch, + level: myDevice.currentLevel] + break + default: + log.debug "no devices found of type $deviceType" + resp = [error: "no device type found of type $deviceType"] + } + return resp +} + +def startAction() { + def action = params.action + def deviceID = params.id + def deviceType = params.deviceType + def result + + switch (deviceType){ + case "lock": + def targetDevice = myLock.find{ it.id == deviceID } + if (targetDevice){ + result = invokeLockAction(targetDevice, action) + } + break + case "switch": + def targetDevice = mySwitch.find{ it.id == deviceID} + result = invokeSwitchAction(targetDevice, action) + break + case "plug": + def targetDevice = myPlug.find{ it.id == deviceID} + result = invokeSwitchAction(targetDevice, action) + break + case "dimmer": + def targetDevice = myDimmer.find{ it.id == deviceID} + if (action.isInteger()){ + result = invokeDimmerAction(targetDevice, action.toInteger()) + }else{ + result = invokeDimmerAction(targetDevice, action) + } + break + case "button": + def targetDevice = myButton.find{ it.id == deviceID} + result = invokeButtonAction(targetDevice, action) + break + case "music": + def targetDevice = myMusic.find{ it.id == deviceID} + result = invokeMusicAction(targetDevice, action) + break + //working on stat + case "thermostat": + def targetDevice = myThermostat.find{ it.id == deviceID} + result = invokeThermostatAction(targetDevice, action) + break + case "sensor": + result = [Code:"1", Message: "No Actions available for Sensors"] + break + default: + log.debug "No device found for $deviceType" + result = [Code:"1", Message:"No Device Found for $deviceType"] + } + return [completed: result] + +} + +def invokeSwitchAction(device, action){ + def result + switch (action){ + case "toggleSwitch": + result = toggleSwitch(device) + break + default: + result = "no action, $action" + log.debug "no action" + } + return result +} + +def invokeThermostatAction(device, action){ + def result + switch (action){ + case "resume": + device.resumeProgram() + result = action + break + case "auto": + device.auto() + result = action + break + case "heat": + device.heat() + result = action + break + case "cool": + device.cool() + result = action + break + case "off": + device.off() + result = action + break + case "emergency heat": + device.emergencyHeat() + result = action + break + case "fanauto": + device.fanAuto() + result = action + break + case "fanon": + device.fanOn() + result = action + break + case "fancirculate": + device.fanCirculate() + result = action + break + case ~/heat-(.*)/: + def values = action.tokenize('-') + def temp = values[1] + result = "set heat point failed" + if (temp.isDouble()){ + temp = temp.toDouble()/10 + device.setHeatingSetpoint(temp) + result = temp + } + break + case ~/cool-(.*)/: + def values = action.tokenize('-') + def temp = values[1] + result = "set cool point failed" + if (temp.isDouble()){ + temp = temp.toDouble()/10 + device.setCoolingSetpoint(temp) + result = temp + } + break + default: + log.debug "no action" + } +} + +def invokeButtonAction(device, action){ + switch (action){ + case "s1": + break + case "s2": + break + case "s3": + break + case "s4": + break + case "l1": + break + case "l2": + break + case "l3": + break + case "l4": + break + default: + log.debug "no action" + } +} + +def invokeMusicAction(device, action){ + def result = action + switch (action){ + case "play": + device.play() + result = action + break + case "stop": + device.stop() + break + case "pause": + device.pause() + result = action + break + case "next": + device.nextTrack() + result = action + break + case "previous": + device.previousTrack() + result = action + break + case 0..100: + device.setLevel(action) + result = action + break + case "mute": + device.mute() + result = action + break + case "unmute": + device.unmute() + result = action + break + default: + log.debug "no action" + result = "no action found, $action" + } + return result +} + +def invokeDimmerAction(device, action){ + def result + log.info "performing ${action}" + switch (action){ + case "toggleSwitch": + result = toggleSwitch(device) + break + case 0..100: + device.setLevel(action) + result = action + break + default: + result = "No Action Found Matching $action" + log.debug "no action" + } + return result +} + +def invokeLockAction(device, action){ + def result + switch (action){ + case "lock": + device.lock() + result = "locked" + break + case "unlock": + device.unlock() + result = "unlocked" + break + default: + result = "no action found, $action" + log.debug "no action" + } + return result +} + +def toggleSwitch(device){ + def result + if (device.currentSwitch == "on") { + device.off() + result = "off" + }else{ + device.on() + result = "on" + } + return result +} + + +def getDevices(){ + def resp = [] + if (myButton){ + myButton.each { + resp << [device: it, class:'button'] + } + } + if (myThermostat){ + myThermostat.each { + resp << [device: it, + class:'thermostat', + temp:it.currentTemperature, + mode: it.currentThermostatMode, + fan: it.currentThermostatFanMode, + tempUnit: it.currentDeviceTemperatureUnit, + humidity: it.currentHumidity, + heatSetpoint: it.currentHeatingSetpoint, + coolSetpoint: it.currentCoolingSetpoint, + minCoolSetpoint: it.currentMinCoolingSetpoint, + maxCoolSetpoint: it.currentMaxCoolingSetpoint, + minHeatSetpoint: it.currentMinHeatingSetpoint, + maxHeatSetpoint: it.currentMaxHeatingSetpoint + ] + } + } + if (mySwitch){ + mySwitch.each { + resp << [device: it, class:'switch', status:it.currentSwitch] + } + } + if (myPlug){ + myPlug.each { + resp << [device: it, class:'plug', status:it.currentSwitch] + } + } + if (myDimmer){ + myDimmer.each { + resp << [device: it, class:'dimmer', status:it.currentSwitch, level:it.currentLevel] + } + } + if (mySensor){ + mySensor.each { + resp << [device: it, class:'sensor', contact: it.currentContact] + } + } + if (myMotion){ + myMotion.each{ + resp << [device: it, class:'sensor', motion: it.currentMotion] + } + } + if (myTemperature){ + myTemperature.each{ + resp << [device: it, class:'sensor', temp: it.currentTemperature] //, attributes:attrs] + } + } + if (myCO2) { + myCO2.each{ + resp << [device: it, class:'sensor', co2: it.currentCarbonDioxide] + } + } + if (myBattery){ + myBattery.each{ + resp << [device: it, class:'sensor', battery: it.currentBattery] + } + } + if (mySmoke){ + mySmoke.each{ smoke-> + resp << [device: smoke, class:'sensor', smoke: smoke.currentSmoke] + } + } + if (myLock){ + myLock.each { + resp << [device: it, class:'lock', status: it.currentLock] + } + } + return resp +} + +def getRoutines(){ + def resp = [] + def routines = location.helloHome?.getPhrases()*.label + def currentMode = location.getCurrentMode().name + resp << [currentMode:currentMode, routines:routines] + log.debug("routines available ${routines}") + log.debug("current mode is ${currentMode}") + return resp +} + +def executeRoutine(){ + def routine = params.routine + location.helloHome?.execute(routine) + return [completed: routine] +} + +def installed() { + log.debug "Installed with settings: ${settings}" + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + initialize() +} + +def initialize() { + +}