diff --git a/.gitignore b/.gitignore index 58bcbf8..13e9ce2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,32 +1,36 @@ -# Windows image file caches -Thumbs.db -ehthumbs.db - -# Folder config file -Desktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msm -*.msp - -# ========================= -# Operating System Files -# ========================= - -# OSX -# ========================= - +#Cloud9 +.c9/ + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# ========================= +# Operating System Files +# ========================= + +# OSX +# ========================= + .DS_Store .AppleDouble .LSOverride # Icon must end with two \r -Icon +Icon + # Thumbnails ._* diff --git a/ButtonsAsPIN.app.groovy b/ButtonsAsPIN.app.groovy new file mode 100644 index 0000000..d0ef664 --- /dev/null +++ b/ButtonsAsPIN.app.groovy @@ -0,0 +1,357 @@ +/** + * Use Buttons As PIN Input + * + * Assign a multi-button controller (e.g., Aeon Labs Minimote) to be a security 'PIN code' input pad, + * which triggers a switch, lock, mode, or Hello Home action. + * More details on GitHub: + * and on SmartThings Community Forum: + * + * Filename: ButtonsAsPIN.app.groovy + * Version: see myVersion() + * Date: 2014-12-22 + * Status: + * - Beta release to Community for testing, feedback, feature requests. + * - Currently hard limited to 3-9 digits from a choice of 1-4. + * - Tested only with 4-button Aeon Labs Minimote, button-push only, no support for button-hold. + * + * Summary Changelog (See github for full Release Notes): + * - tbd. + * + * Author: Terry Gauchat (CosmicPuppy) + * Email: terry@cosmicpuppy.com + * SmartThings Community: @tgauchat -- + * Latest versions on GitHub at: + * + * There is no charge for this software: + * Optional "encouragement funding" is accepted to PayPal address: info@CosmicPuppy.com + * (Contributions help cover endless vet bills for Buddy & Deuce, the official CosmicPuppy beagles.) + * + * ---------------------------------------------------------------------------------------------------------------- + * Copyright 2014 Terry Gauchat + * + * 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. + */ + + +import groovy.json.JsonSlurper + +/** + * Frequently edited options, parameters, constants. + */ +private def myVersion() { return "v0.1.0-beta+005" } +/** + * Disable specific level of logging by commenting out log.* expressions as desired. + * NB: Someday SmartThings's live log viewer front-end should provide dynamic filter-by-level, right? + */ +private def myDebug(text) { + // log.debug myLogFormat(text) +} +private def myTrace(text) { + log.trace myLogFormat(text) +} +private def myInfo(text) { + log.info myLogFormat(text) +} +private def myLogFormat(text) { + return "\"${app.label}\".(\"${app.name}\"): ${text}" +} + + +/** + * Definiton + */ +definition( + name: "Use Buttons As PIN Input", + namespace: "CosmicPuppy", + author: "Terry Gauchat", + description: "Assign a multi-button controller (e.g., Aeon Labs Minimote) to be a security 'PIN code' input pad, " + + "which triggers a switch, lock, mode, or Hello Home action.", + category: "Safety & Security", + iconUrl: "http://cosmicpuppy.com/SmartThingsAssets/ButtonsAsPIN_icon_ComboLock.jpg", + iconX2Url: "http://cosmicpuppy.com/SmartThingsAssets/ButtonsAsPIN_icon_ComboLock.jpg", + iconX3Url: "http://cosmicpuppy.com/SmartThingsAssets/ButtonsAsPIN_icon_ComboLock.jpg", +) /* definition */ + + +preferences { + page(name: "pageSelectButtonDev") + page(name: "pageSetPinSequence") + page(name: "pageSelectActions") +} /* preferences */ + + +def pageSelectButtonDev() { + dynamicPage(name: "pageSelectButtonDev", title: "Select your button device & PIN length...", + nextPage: "pageSetPinSequence", uninstall: true) { + section { + paragraph "App Info: Version ${myVersion()}\nhttps://github.com/CosmicPuppy/SmartThings-ButtonsAsPIN" + } + section([mobileOnly:true]) { + label title: "Assign a name to this SmartApp instance?", required: false + mode title: "Activate for specific mode(s)?", required: false + } + section { + input name: "buttonDevice", type: "capability.button", title: "Button Device:", multiple: false, required: true + } + section { + input name: "pinLength", type: "enum", title: "Desired PIN length (3 to 9 digits):", multiple: false, + required: true, options:["3","4","5","6","7","8","9"], defaultValue: "4"; + } + } +} /* pageSelectButtonDev() */ + + +def pageSetPinSequence() { + dynamicPage(name: "pageSetPinSequence", title: "Set PIN (security code sequence)...", nextPage: "pageSelectActions", + install: false, uninstall: true) { + section("PIN Code Buttons in Desired Sequence Order") { + L:{ for( i in 1 .. pinLength.toInteger() ) { + input name: "comb_${i}", type: "enum", title: "Sequence $i:", mulitple: false, required: true, + options: ["1","2","3","4"]; + } + } + href "pageSelectButtonDev", title:"Go Back", description:"Tap to go back." + } + } +} /* pageSetPinSequence() */ + + +def pageSelectActions() { + def valid = true + def pageProperties = [ + name: "pageSelectActions", + title: "Confirm PIN & Select Action(s)...", + install: true, + uninstall: true + ] + + /** + * TODO: This should be dynamic length loop, but I need to figure out how to dynamically String substitute comb_*, + * -- Is that even possible!? Maybe some form of Eval() would work? + */ + state.pinSeqList = [] + state.pinLength = pinLength.toInteger() + switch ( state.pinLength ) { + case 9: + state.pinSeqList << comb_9 + case 8..9: + state.pinSeqList << comb_8 + case 7..9: + state.pinSeqList << comb_7 + case 6..9: + state.pinSeqList << comb_6 + case 5..9: + state.pinSeqList << comb_5 + case 4..9: + state.pinSeqList << comb_4 + case 3..9: + state.pinSeqList << comb_3 + case 2..9: + state.pinSeqList << comb_2 + case 1..9: + state.pinSeqList << comb_1 + } + state.pinSeqList.reverse(true) // true --> mutate original list instead of a copy. + myDebug("pinSeqList is $state.pinSeqList") + myDebug("pinLength is $state.pinLength") + + return dynamicPage(pageProperties) { + section() { + paragraph "PIN Code set to: " + "$state.pinSeqList" + href "pageSetPinSequence", title:"Go Back", description:"Tap to change PIN Code sequence." + } + section("Devices, Modes, Actions") { + input "switches", "capability.switch", title: "Toggle Lights & Switches", multiple: true, required: false + input "locks", "capability.lock", title: "Toggle Locks", multiple: true, required: false + input "mode", "mode", title: "Set Mode", required: false + def phrases = location.helloHome?.getPhrases()*.label + if (phrases) { + myDebug("Phrase list found: ${phrases}") + /* NB: Customary to not allow multiple phrases. Complications due to sequencing, etc. */ + input "phrase", "enum", title: "Trigger Hello Home Action", required: false, options: phrases + } + } + } +} /* pageSelectActions() */ + + +def installed() { + myTrace("Installed; Version: ${myVersion()}") + myDebug("Installed; settings: ${settings}") // settings includes the PIN, so we should avoid logging except for Debug. + + initialize() +} + +def updated() { + myTrace("Updated; Version: ${myVersion()}") + myDebug("Updated; settings: ${settings}") // settings includes the PIN, so we should avoid logging except for Debug. + + unsubscribe() + initialize() +} + +def initialize() { + subscribe(buttonDevice, "button", buttonEvent) + state.inputDigitsList = [] + + myDebug("Initialized - state: ${state}") +} + + +/** + * Watch for correct matching PIN input by rolling scan of last "pinLength" presses. + * + * TODO: Keep a count of the number of unsucessful sequences so that alarm or other alert action could be called. + * Such an alert could also (virtually) "disable" the buttons for a period of time. + * + * NB: It would be more secure to require a Start and/or End indicator, but complicates user interface. + * One possible improvement is to have a "timeout" on the input buffer. + * + * NB: On the Aeon Minimote, pressing the same key twice is "sort of" filtered unless + * you wait for the red LED confirmation response. + * The two presses are probably detectable by analyzing the buttonDevice.events log, but the stream seems inconsistent. + * Therefore the User "MUST" wait for confirmation after each keypress else input digits may be lost (undetected). + * NOT waiting will often still work, though, if there are no double presses (duplicate sequential digits in the PIN). + */ +def buttonEvent(evt){ + def allOK = true; + if(true) { + def value = evt.value + def slurper = new JsonSlurper() + def dataMap = slurper.parseText(evt.data) + def buttonNumber = dataMap.buttonNumber + myDebug("buttonEvent Device: [$buttonDevice.name], Name: [$evt.name], Value: [$evt.value], Data: [$evt.data], ButtonNumber: [$dataMap.buttonNumber]") + if(value == "pushed") { + state.inputDigitsList << buttonNumber.toString() + if(state.inputDigitsList.size > state.pinLength) { + state.inputDigitsList.remove(state.inputDigitsList.size - state.pinLength - 1) + } + myDebug("Current inputDigitsList: $state.inputDigitsList") + if(state.inputDigitsList.equals(state.pinSeqList)) { + myDebug("PIN Match Detected; found [$state.pinSeqList]. Clearing input digits buffer.") + myTrace("PIN Match Detected. Clearing input digits buffer.") + state.inputDigitsList.clear(); + executeHandlers() + } else { + myDebug("No PIN match yet: inputDigitsList is $inputDigitsList; looking for $state.pinSeqList") + myTrace("No PIN match yet.") + } + } + + /** + * TODO: (Experimental code for reference): + * If the above code misses button presses that occur too quickly, + * considering scanning back through the event log. + * The behavior if this is a little confusing though: Repeated keys show up in the recentEvents(). + * Could we limit data entry to 10 or 20 seconds and limit the backscan to the length of the PIN? + * The only time multiple event backscan seems to apply is for multi-presses of the same key. But then this is essential. + * Yet eventsSince seems to only be reporting NEW events. Weird. Not critical for this App to work ok, though. + */ + // def recentEvents = buttonDevice.eventsSince(new Date(now() - 10000), + // [max:pinLength.toInteger()]).findAll{it.value == evt.value && it.data == evt.data} + // myDebug("PIN Found ${recentEvents.size()?:0} events in past 10 seconds" + // recentEvents.eachWithIndex { it, i -> myDebug("PIN [$i] Value:$it.value Data:$it.data" } + } +} /* buttonEvent() */ + + +/** + * Event handlers. + * Most code copied from "Button Controller" by SmartThings, + slight modifications. + */ +def executeHandlers() { + myTrace("executeHandlers; switches/locks toggles, mode set, phrase execute.") + + def switches = findPreferenceSetting('switches') + myDebug("switches are ${switches}") + if (switches != null) toggle(switches,'switch') + + def locks = findPreferenceSetting('locks') + myDebug("locks are ${locks}") + if (locks != null) toggle(locks,'lock') + + def mode = findPreferenceSetting('mode') + if (mode != null) changeMode(mode) + + def phrase = findPreferenceSetting('phrase') + if (phrase != null) { + myTrace("helloHome.execute: \"${phrase}\"") + location.helloHome.execute(phrase) + } +} /* executeHandlers() */ + + +def findPreferenceSetting(preferenceName) { + def pref = settings[preferenceName] + if(pref != null) { + myDebug("Found Pref Setting: $pref for $preferenceName") + } + return pref +} + + +/** + * NB: This function only works properly if "devices" list passed are all of same capability. + * Shouldn't be a problem, because devices is from a preference setting list filtered by capability. + * NB: "Toggle" is a misnomer as it sets all switches to off ANY are on (rather than check and toggle each one's state). + * (Similarly, all locks are unlocked if any are found locked.) + * Is toggling the most intuitive behavior since the resulting set of states is possibly uncertain to the user? + * A possible "improvement"?: Require two different PINs, for lock vs for unlock (could just be the last digit: 1 vs 2). + * This would also better accomodate two distinct Hello Home phrases (activating security vs deactiviting security). + * But: A toggle type action for Hello Home phrase is appropriate if reading and using mode or state is reliable. + * The current "Failsafe default" sections are a questionable design decision; Is there a better choice? + */ +def toggle(devices,capabilityType) { + if (capabilityType == 'switch') { + myDebug("toggle switch Values: $devices = ${devices*.currentValue('switch')}") + if (devices*.currentValue('switch').contains('on')) { + myTrace("Set devices.off: ${devices}") + devices.off() + } + else if (devices*.currentValue('switch').contains('off')) { + myTrace("Set devices.on: ${devices}") + devices.on() + } + else { + myTrace("Set devices.on (Failsafe default action attempt.): ${devices}") + devices.on() + } + } + else if (capabilityType == 'lock') { + myDebug("toggle lock Values: $devices = ${devices*.currentValue('lock')}") + if (devices*.currentValue('lock').contains('locked')) { + myTrace("Set devices.unlock: ${devices}") + devices.unlock() + } + else if (devices*.currentValue('lock').contains('unlocked')) { + myTrace("Set devices.lock: ${devices}") + devices.lock() + } + else { + myTrace("Set devices.unlock (Failsafe default action attempt.): ${devices}") + devices.unlock() + } + } +} /* toggle() */ + + +def changeMode(mode) { + myDebug("changeMode: $mode, location.mode = $location.mode, location.modes = $location.modes") + + if (location.mode != mode && location.modes?.find { it.name == mode }) { + myTrace("setLocationMode: ${mode}") + setLocationMode(mode) + } +} + + +/* =========== */ +/* End of File */ +/* =========== */ \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..e06d208 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..d819b5d --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +SmartThings-ButtonPIN +===================== + +Use Buttons As PIN Input + +Assign a multi-button controller (e.g., Aeon Labs Minimote) to be a security 'PIN code' input pad, + which triggers a switch, lock, mode, or Hello Home action. + +More details on SmartThings Community Forum: + + +Status: + - First Beta release to Community for testing, feedback, feature requests. + - Currently hard limited to 3-9 digits from a choice of 1-4. + - Tested only with 4-button Aeon Labs Minimote, button-push only, no support for button-hold. + +Summary Changelog (See github for Release Notes): + - tbd. \ No newline at end of file