mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-16 13:10:51 +00:00
1532 lines
47 KiB
Groovy
1532 lines
47 KiB
Groovy
/**
|
|
* User Lock Manager v4.1.5
|
|
*
|
|
* Copyright 2015 Erik Thayer
|
|
* Keypad support added by BLebson
|
|
* Door manual unlock notifications and option by DimitriRodis 2016-06-24
|
|
*
|
|
*
|
|
*/
|
|
definition(
|
|
name: "User Lock Manager",
|
|
namespace: "ethayer",
|
|
author: "Erik Thayer",
|
|
description: "This app allows you to change, delete, and schedule user access.",
|
|
category: "Safety & Security",
|
|
iconUrl: "https://dl.dropboxusercontent.com/u/54190708/LockManager/lockmanager.png",
|
|
iconX2Url: "https://dl.dropboxusercontent.com/u/54190708/LockManager/lockmanagerx2.png",
|
|
iconX3Url: "https://dl.dropboxusercontent.com/u/54190708/LockManager/lockmanagerx3.png")
|
|
|
|
import groovy.json.JsonSlurper
|
|
import groovy.json.JsonBuilder
|
|
|
|
preferences {
|
|
page(name: "rootPage")
|
|
page(name: "setupPage")
|
|
page(name: "userPage")
|
|
page(name: "notificationPage")
|
|
page(name: "onUnlockPage")
|
|
page(name: "schedulingPage")
|
|
page(name: "calendarPage")
|
|
page(name: "resetAllCodeUsagePage")
|
|
page(name: "resetCodeUsagePage")
|
|
page(name: "reEnableUserPage")
|
|
page(name: "infoPage")
|
|
page(name: "keypadPage")
|
|
page(name: "infoRefreshPage")
|
|
page(name: "lockInfoPage")
|
|
}
|
|
|
|
def rootPage() {
|
|
//reset errors on each load
|
|
dynamicPage(name: "rootPage", title: "", install: true, uninstall: true) {
|
|
|
|
section("Which Locks?") {
|
|
input "theLocks","capability.lockCodes", title: "Select Locks", required: true, multiple: true, submitOnChange: true
|
|
}
|
|
|
|
if (theLocks) {
|
|
initalizeLockData()
|
|
|
|
section {
|
|
input name: "maxUsers", title: "Number of users", type: "number", multiple: false, refreshAfterSelection: true, submitOnChange: true
|
|
href(name: "toSetupPage", title: "User Settings", page: "setupPage", description: setupPageDescription(), state: setupPageDescription() ? "complete" : "")
|
|
href(name: "toInfoPage", page: "infoPage", title: "Lock Info")
|
|
href(name: "toKeypadPage", page: "keypadPage", title: "Keypad Info (optional)")
|
|
href(name: "toNotificationPage", page: "notificationPage", title: "Notification Settings", description: notificationPageDescription(), state: notificationPageDescription() ? "complete" : "")
|
|
href(name: "toSchedulingPage", page: "schedulingPage", title: "Schedule (optional)", description: schedulingHrefDescription(), state: schedulingHrefDescription() ? "complete" : "")
|
|
href(name: "toOnUnlockPage", page: "onUnlockPage", title: "Global Hello Home")
|
|
}
|
|
section {
|
|
label(title: "Label this SmartApp", required: false, defaultValue: "")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
def setupPage() {
|
|
dynamicPage(name:"setupPage", title:"User Settings") {
|
|
if (maxUsers > 0) {
|
|
section('Users') {
|
|
(1..maxUsers).each { user->
|
|
if (!state."userState${user}") {
|
|
//there's no values, so reset
|
|
resetCodeUsage(user)
|
|
}
|
|
if (settings."userCode${user}" && settings."userSlot${user}") {
|
|
getConflicts(settings."userSlot${user}")
|
|
}
|
|
href(name: "toUserPage${user}", page: "userPage", params: [number: user], required: false, description: userHrefDescription(user), title: userHrefTitle(user), state: userPageState(user) )
|
|
}
|
|
}
|
|
section {
|
|
href(name: "toResetAllCodeUsage", title: "Reset Code Usage", page: "resetAllCodeUsagePage", description: "Tap to reset")
|
|
}
|
|
} else {
|
|
section("Users") {
|
|
paragraph "Users are set to zero. Please go back to the main page and change the number of users to at least 1."
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
def userPage(params) {
|
|
dynamicPage(name:"userPage", title:"User Settings") {
|
|
def i = getUser(params);
|
|
|
|
if (!state."userState${i}".enabled) {
|
|
section {
|
|
paragraph "WARNING:\n\nThis user has been disabled.\nReason: ${state."userState${i}".disabledReason}"
|
|
href(name: "toreEnableUserPage", title: "Reset User", page: "reEnableUserPage", params: [number: i], description: "Tap to reset")
|
|
}
|
|
}
|
|
if (settings."userCode${i}" && settings."userSlot${i}") {
|
|
def conflict = getConflicts(settings."userSlot${i}")
|
|
if (conflict.has_conflict) {
|
|
section("Conflicts:") {
|
|
theLocks.each { lock->
|
|
if (conflict."lock${lock.id}" && conflict."lock${lock.id}".conflicts != []) {
|
|
paragraph "${lock.displayName} slot ${fancyString(conflict."lock${lock.id}".conflicts)}"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
section("Code #${i}") {
|
|
input(name: "userName${i}", type: "text", title: "Name for User", defaultValue: settings."userName${i}")
|
|
def title = "Code (4 to 8 digits)"
|
|
theLocks.each { lock->
|
|
if (lock.hasAttribute('pinLength')) {
|
|
title = "Code (Must be ${lock.latestValue('pinLength')} digits)"
|
|
}
|
|
}
|
|
input(name: "userCode${i}", type: "text", title: title, required: false, defaultValue: settings."userCode${i}", refreshAfterSelection: true)
|
|
input(name: "userSlot${i}", type: "number", title: "Slot (1 through 30)", defaultValue: preSlectedCode(i))
|
|
}
|
|
section {
|
|
input(name: "dontNotify${i}", title: "Mute entry notification?", type: "bool", required: false, defaultValue: settings."dontNotify${i}")
|
|
input(name: "burnCode${i}", title: "Burn after use?", type: "bool", required: false, defaultValue: settings."burnCode${i}")
|
|
input(name: "userEnabled${i}", title: "Enabled?", type: "bool", required: false, defaultValue: settings."userEnabled${i}")
|
|
def hhPhrases = location.getHelloHome()?.getPhrases()*.label
|
|
if (hhPhrases) {
|
|
hhPhrases.sort()
|
|
input name: "userHomePhrases${i}", type: "enum", title: "Hello Home Phrase", multiple: true,required: false, options: hhPhrases, defaultValue: settings."userHomePhrases${i}", refreshAfterSelection: true
|
|
input "userNoRunPresence${i}", "capability.presenceSensor", title: "Don't run Actions if any of these are present:", multiple: true, required: false, defaultValue: settings."userNoRunPresence${i}" || false
|
|
input "userDoRunPresence${i}", "capability.presenceSensor", title: "Run Actions only if any of these are present:", multiple: true, required: false, defaultValue: settings."userDoRunPresence${i}" || false
|
|
}
|
|
}
|
|
section {
|
|
href(name: "toSetupPage", title: "Back To Users", page: "setupPage")
|
|
href(name: "toResetCodeUsagePage", title: "Reset Code Usage", page: "resetCodeUsagePage", params: [number: i], description: "Tap to reset")
|
|
}
|
|
}
|
|
}
|
|
|
|
def preSlectedCode(i) {
|
|
if (settings."userSlot${i}" != null) {
|
|
return settings."userSlot${i}"
|
|
} else {
|
|
return i
|
|
}
|
|
}
|
|
|
|
def notificationPage() {
|
|
dynamicPage(name: "notificationPage", title: "Notification Settings") {
|
|
|
|
section {
|
|
input(name: "phone", type: "text", title: "Text This Number", description: "Phone number", required: false, submitOnChange: true)
|
|
paragraph "For multiple SMS recipients, separate phone numbers with a semicolon(;)"
|
|
input(name: "notification", type: "bool", title: "Send A Push Notification", description: "Notification", required: false, submitOnChange: true)
|
|
if (phone != null || notification || sendevent) {
|
|
input(name: "notifyAccess", title: "on User Entry", type: "bool", required: false)
|
|
input(name: "notifyLock", title: "on Lock", type: "bool", required: false)
|
|
input(name: "notifyUnlock", title: "on Unlock", type: "bool", required: false)
|
|
input(name: "notifyAccessStart", title: "when granting access", type: "bool", required: false)
|
|
input(name: "notifyAccessEnd", title: "when revoking access", type: "bool", required: false)
|
|
}
|
|
}
|
|
|
|
section("Only During These Times (optional)") {
|
|
input(name: "notificationStartTime", type: "time", title: "Notify Starting At This Time", description: null, required: false)
|
|
input(name: "notificationEndTime", type: "time", title: "Notify Ending At This Time", description: null, required: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
def schedulingPage() {
|
|
dynamicPage(name: "schedulingPage", title: "Rules For Access Scheduling") {
|
|
if (!days) {
|
|
section {
|
|
href(name: "toCalendarPage", title: "Calendar", page: "calendarPage", description: calendarHrefDescription(), state: calendarHrefDescription() ? "complete" : "")
|
|
}
|
|
}
|
|
if (!startDay && !startMonth && !startYear && !endDay && !endMonth && !endYear) {
|
|
section {
|
|
input(name: "days", type: "enum", title: "Allow User Access On These Days", description: "Every day", required: false, multiple: true, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], submitOnChange: true)
|
|
}
|
|
section {
|
|
input(name: "modeStart", title: "Allow Access when in this mode", type: "mode", required: false, mutliple: false, submitOnChange: true)
|
|
}
|
|
section {
|
|
if (modeStart) {
|
|
input "andOrTime", "enum", title: "[And/Or] at a set time?", metadata:[values:["and", "or"]], required: false, submitOnChange: true
|
|
}
|
|
if ((modeStart == null) || andOrTime) {
|
|
input(name: "startTime", type: "time", title: "Start Time", description: null, required: false)
|
|
input(name: "endTime", type: "time", title: "End Time", description: null, required: false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
def calendarPage() {
|
|
dynamicPage(name: "calendarPage", title: "Calendar Access") {
|
|
section() {
|
|
paragraph "This page is for advanced users only. You must enter each field carefully."
|
|
paragraph "Calendar use does not support daily grant/deny OR Modes. You cannot both have a date here, and allow access only on certain days/modes."
|
|
}
|
|
def hhPhrases = location.getHelloHome()?.getPhrases()*.label
|
|
section("Start Date") {
|
|
input name: "startDay", type: "number", title: "Day", required: false
|
|
input name: "startMonth", type: "number", title: "Month", required: false
|
|
input name: "startYear", type: "number", description: "Format(yyyy)", title: "Year", required: false
|
|
input name: "startTime", type: "time", title: "Start Time", description: null, required: false
|
|
if (hhPhrases) {
|
|
hhPhrases.sort()
|
|
input name: "calStartPhrase", type: "enum", title: "Hello Home Phrase", multiple: true,required: false, options: hhPhrases, refreshAfterSelection: true
|
|
}
|
|
}
|
|
section("End Date") {
|
|
input name: "endDay", type: "number", title: "Day", required: false
|
|
input name: "endMonth", type: "number", title: "Month", required: false
|
|
input name: "endYear", type: "number", description: "Format(yyyy)", title: "Year", required: false
|
|
input name: "endTime", type: "time", title: "End Time", description: null, required: false
|
|
if (hhPhrases) {
|
|
hhPhrases.sort()
|
|
input name: "calEndPhrase", type: "enum", title: "Hello Home Phrase", multiple: true,required: false, options: hhPhrases, refreshAfterSelection: true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
def onUnlockPage() {
|
|
dynamicPage(name:"onUnlockPage", title:"Global Actions (Any Code)") {
|
|
section("Actions") {
|
|
def hhPhrases = location.getHelloHome()?.getPhrases()*.label
|
|
if (hhPhrases) {
|
|
hhPhrases.sort()
|
|
input name: "homePhrases", type: "enum", title: "Home Mode Phrase", multiple: true, required: false, options: hhPhrases, refreshAfterSelection: true, submitOnChange: true
|
|
if (homePhrases) {
|
|
input "noRunPresence", "capability.presenceSensor", title: "Don't run Actions if any of these are present:", multiple: true, required: false
|
|
input "doRunPresence", "capability.presenceSensor", title: "Run Actions only if any of these are present:", multiple: true, required: false
|
|
input name: "manualUnlock", title: "Initiate phrase on manual unlock also?", type: "bool", defaultValue: false, refreshAfterSelection: true
|
|
input(name: "modeIgnore", title: "Do not run Routine when in this mode", type: "mode", required: false, mutliple: false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
def resetCodeUsagePage(params) {
|
|
def i = getUser(params)
|
|
// do reset
|
|
resetCodeUsage(i)
|
|
|
|
dynamicPage(name:"resetCodeUsagePage", title:"User Usage Reset") {
|
|
section {
|
|
paragraph "User code usage has been reset."
|
|
}
|
|
section {
|
|
href(name: "toSetupPage", title: "Back To Users", page: "setupPage")
|
|
}
|
|
}
|
|
}
|
|
|
|
def resetAllCodeUsagePage() {
|
|
// do resetAll
|
|
resetAllCodeUsage()
|
|
dynamicPage(name:"resetAllCodeUsagePage", title:"User Settings") {
|
|
section {
|
|
paragraph "All user code usages have been reset."
|
|
}
|
|
section("Users") {
|
|
href(name: "toSetupPage", title: "Back to Users", page: "setupPage")
|
|
href(name: "toRootPage", title: "Main Page", page: "rootPage")
|
|
}
|
|
}
|
|
}
|
|
|
|
def reEnableUserPage(params) {
|
|
// do reset
|
|
def i = getUser(params)
|
|
enableUser(i)
|
|
lockErrorLoopReset()
|
|
dynamicPage(name:"reEnableUserPage", title:"User re-enabled") {
|
|
section {
|
|
paragraph "User has been enabled."
|
|
}
|
|
section {
|
|
href(name: "toSetupPage", title: "Back To Users", page: "setupPage")
|
|
}
|
|
}
|
|
}
|
|
|
|
def getUser(params) {
|
|
def i = 1
|
|
// Assign params to i. Sometimes parameters are double nested.
|
|
if (params.number) {
|
|
i = params.number
|
|
} else if (params.params){
|
|
i = params.params.number
|
|
} else if (state.lastUser) {
|
|
i = state.lastUser
|
|
}
|
|
|
|
//Make sure i is a round number, not a float.
|
|
if ( ! i.isNumber() ) {
|
|
i = i.toInteger();
|
|
} else if ( i.isNumber() ) {
|
|
i = Math.round(i * 100) / 100
|
|
}
|
|
state.lastUser = i
|
|
return i
|
|
}
|
|
|
|
def getLock(params) {
|
|
def id = ''
|
|
// Assign params to id. Sometimes parameters are double nested.
|
|
if (params.id) {
|
|
id = params.id
|
|
} else if (params.params){
|
|
id = params.params.id
|
|
} else if (state.lastLock) {
|
|
id = state.lastLock
|
|
}
|
|
|
|
state.lastLock = id
|
|
return theLocks.find{it.id == id}
|
|
}
|
|
|
|
def infoPage() {
|
|
dynamicPage(name:"infoPage", title:"Lock Info") {
|
|
section() {
|
|
href(name: "toInfoRefreshPage", page: "infoRefreshPage", title: "Refresh Lock Data", description: 'Tap to refresh')
|
|
}
|
|
section("Locks") {
|
|
if (theLocks) {
|
|
def i = 0
|
|
theLocks.each { lock->
|
|
i++
|
|
href(name: "toLockInfoPage${i}", page: "lockInfoPage", params: [id: lock.id], required: false, title: lock.displayName )
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
def infoRefreshPage() {
|
|
dynamicPage(name:"infoRefreshPage", title:"Lock Info") {
|
|
section() {
|
|
manualPoll()
|
|
paragraph "Lock info refreshing soon."
|
|
href(name: "toInfoPage", page: "infoPage", title: "Back to Lock Info")
|
|
}
|
|
}
|
|
}
|
|
|
|
def lockInfoPage(params) {
|
|
dynamicPage(name:"lockInfoPage", title:"Lock Info") {
|
|
|
|
def lock = getLock(params)
|
|
if (lock) {
|
|
section("${lock.displayName}") {
|
|
if (state."lock${lock.id}".codes != null) {
|
|
def i = 0
|
|
def pass = ''
|
|
state."lock${lock.id}".codes.each { code->
|
|
i++
|
|
pass = state."lock${lock.id}".codes."slot${i}"
|
|
paragraph "Slot ${i}\nCode: ${pass}"
|
|
}
|
|
} else {
|
|
paragraph "No Lock data received yet. Requires custom device driver. Will be populated on next poll event."
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
def keypadPage() {
|
|
dynamicPage(name: "keypadPage",title: "Keypad Settings (optional)") {
|
|
section("Settings") {
|
|
// TODO: put inputs here
|
|
input(name: "keypad", title: "Keypad", type: "capability.lockCodes", multiple: true, required: false)
|
|
}
|
|
def hhPhrases = location.getHelloHome()?.getPhrases()*.label
|
|
hhPhrases?.sort()
|
|
section("Routines", hideable: true, hidden: true) {
|
|
input(name: "armRoutine", title: "Arm/Away routine", type: "enum", options: hhPhrases, required: false)
|
|
input(name: "disarmRoutine", title: "Disarm routine", type: "enum", options: hhPhrases, required: false)
|
|
input(name: "stayRoutine", title: "Arm/Stay routine", type: "enum", options: hhPhrases, required: false)
|
|
input(name: "nightRoutine", title: "Arm/Night routine", type: "enum", options: hhPhrases, required: false)
|
|
input(name: "armDelay", title: "Arm Delay (in seconds)", type: "number", required: false)
|
|
input(name: "notifyIncorrectPin", title: "Notify you when incorrect code is used?", type: "bool", required: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
public smartThingsDateFormat() { "yyyy-MM-dd'T'HH:mm:ss.SSSZ" }
|
|
|
|
public humanReadableStartDate() {
|
|
new Date().parse(smartThingsDateFormat(), startTime).format("h:mm a", timeZone(startTime))
|
|
}
|
|
public humanReadableEndDate() {
|
|
new Date().parse(smartThingsDateFormat(), endTime).format("h:mm a", timeZone(endTime))
|
|
}
|
|
|
|
def manualPoll() {
|
|
theLocks.poll()
|
|
}
|
|
|
|
def getConflicts(i) {
|
|
def currentCode = settings."userCode${i}"
|
|
def currentSlot = settings."userSlot${i}"
|
|
def conflict = [:]
|
|
conflict.has_conflict = false
|
|
|
|
|
|
theLocks.each { lock->
|
|
if (state."lock${lock.id}".codes) {
|
|
conflict."lock${lock.id}" = [:]
|
|
conflict."lock${lock.id}".conflicts = []
|
|
def ind = 0
|
|
state."lock${lock.id}".codes.each { code ->
|
|
ind++
|
|
if (currentSlot?.toInteger() != ind.toInteger() && !isUnique(currentCode, state."lock${lock.id}".codes."slot${ind}")) {
|
|
conflict.has_conflict = true
|
|
state."userState${i}".enabled = false
|
|
state."userState${i}".disabledReason = "Code Conflict Detected"
|
|
conflict."lock${lock.id}".conflicts << ind
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return conflict
|
|
}
|
|
|
|
def isUnique(newInt, oldInt) {
|
|
|
|
if (newInt == null || oldInt == null) {
|
|
// if either number is null, break here.
|
|
return true
|
|
}
|
|
|
|
if (!newInt.isInteger() || !oldInt.isInteger()) {
|
|
// number is not an integer, can't check.
|
|
return true
|
|
}
|
|
|
|
def newArray = []
|
|
def oldArray = []
|
|
def result = true
|
|
|
|
def i = 0
|
|
// Get a normalized sequence, at the same length
|
|
newInt.toString().toList().collect {
|
|
i++
|
|
if (i <= oldInt.length()) {
|
|
newArray << normalizeNumber(it.toInteger())
|
|
}
|
|
}
|
|
|
|
i = 0
|
|
oldInt.toString().toList().collect {
|
|
i++
|
|
if (i <= oldInt.length()) {
|
|
oldArray << normalizeNumber(it.toInteger())
|
|
}
|
|
}
|
|
|
|
i = 0
|
|
newArray.each { num->
|
|
i++
|
|
if (newArray.join() == oldArray.join()) {
|
|
// The normalized numbers are the same!
|
|
result = false
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
def normalizeNumber(number) {
|
|
def result = null
|
|
// RULE: Since some locks share buttons, make sure unique.
|
|
// Even locks with 10-keys follow this rule! (annoyingly)
|
|
switch (number) {
|
|
case [1,2]:
|
|
result = 1
|
|
break
|
|
case [3,4]:
|
|
result = 2
|
|
break
|
|
case [5,6]:
|
|
result = 3
|
|
break
|
|
case [7,8]:
|
|
result = 4
|
|
break
|
|
case [9,0]:
|
|
result = 5
|
|
break
|
|
}
|
|
return result
|
|
}
|
|
|
|
def setupPageDescription(){
|
|
def parts = []
|
|
for (int i = 1; i <= settings.maxUsers; i++) {
|
|
parts << settings."userName${i}"
|
|
}
|
|
return fancyString(parts)
|
|
}
|
|
|
|
def notificationPageDescription() {
|
|
def parts = []
|
|
def msg = ""
|
|
if (settings.phone) {
|
|
parts << "SMS to ${phone}"
|
|
}
|
|
if (settings.sendevent) {
|
|
parts << "Event Notification"
|
|
}
|
|
if (settings.notification) {
|
|
parts << "Push Notification"
|
|
}
|
|
msg += fancyString(parts)
|
|
parts = []
|
|
|
|
if (settings.notifyAccess) {
|
|
parts << "on entry"
|
|
}
|
|
if (settings.notifyLock) {
|
|
parts << "on lock"
|
|
}
|
|
if (settings.notifyUnlock) {
|
|
parts << "on unlock"
|
|
}
|
|
if (settings.notifyAccessStart) {
|
|
parts << "when granting access"
|
|
}
|
|
if (settings.notifyAccessEnd) {
|
|
parts << "when revoking access"
|
|
}
|
|
if (settings.notificationStartTime) {
|
|
parts << "starting at ${settings.notificationStartTime}"
|
|
}
|
|
if (settings.notificationEndTime) {
|
|
parts << "ending at ${settings.notificationEndTime}"
|
|
}
|
|
if (parts.size()) {
|
|
msg += ": "
|
|
msg += fancyString(parts)
|
|
}
|
|
return msg
|
|
}
|
|
|
|
def calendarHrefDescription() {
|
|
def dateStart = startDateTime()
|
|
def dateEnd = endDateTime()
|
|
if (dateEnd && dateStart) {
|
|
def startReadableTime = readableDateTime(dateStart)
|
|
def endReadableTime = readableDateTime(dateEnd)
|
|
return "Accessible from ${startReadableTime} until ${endReadableTime}"
|
|
} else if (!dateEnd && dateStart) {
|
|
def startReadableTime = readableDateTime(dateStart)
|
|
return "Accessible on ${startReadableTime}"
|
|
} else if (dateEnd && !dateStart){
|
|
def endReadableTime = readableDateTime(dateEnd)
|
|
return "Accessible until ${endReadableTime}"
|
|
}
|
|
}
|
|
|
|
def readableDateTime(date) {
|
|
new Date().parse(smartThingsDateFormat(), date.format(smartThingsDateFormat(), location.timeZone)).format("EEE, MMM d yyyy 'at' h:mma", location.timeZone)
|
|
}
|
|
|
|
def userHrefTitle(i) {
|
|
def title = "User ${i}"
|
|
if (settings."userName${i}") {
|
|
title = settings."userName${i}"
|
|
}
|
|
return title
|
|
}
|
|
|
|
def userHrefDescription(i) {
|
|
def uc = settings."userCode${i}"
|
|
def us = settings."userSlot${i}"
|
|
def usage = state."userState${i}".usage
|
|
def description = ""
|
|
if (us != null) {
|
|
description += "Slot: ${us}"
|
|
}
|
|
if (uc != null) {
|
|
description += " / ${uc}"
|
|
if(settings."burnCode${i}") {
|
|
description += ' [Single Use]'
|
|
}
|
|
}
|
|
if (usage != null) {
|
|
description += " [Usage: ${usage}]"
|
|
}
|
|
return description
|
|
}
|
|
|
|
def userPageState(i) {
|
|
if (settings."userCode${i}" && userIsEnabled(i)) {
|
|
if (settings."burnCode${i}") {
|
|
if (state."userState${i}".usage > 0) {
|
|
return 'incomplete'
|
|
} else {
|
|
return 'complete'
|
|
}
|
|
} else {
|
|
return 'complete'
|
|
}
|
|
|
|
} else if (settings."userCode${i}" && !settings."userEnabled${i}") {
|
|
return 'incomplete'
|
|
} else {
|
|
return 'incomplete'
|
|
}
|
|
}
|
|
|
|
def userIsEnabled(i) {
|
|
if (settings."userEnabled${i}" && (settings."userCode${i}" != null) && (state."userState${i}".enabled != false)) {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
def fancyDeviceString(devices = []) {
|
|
fancyString(devices.collect { deviceLabel(it) })
|
|
}
|
|
|
|
def deviceLabel(device) {
|
|
return device.label ?: device.name
|
|
}
|
|
|
|
def fancyString(listOfStrings) {
|
|
listOfStrings.removeAll([null])
|
|
def fancify = { list ->
|
|
return list.collect {
|
|
def label = it
|
|
if (list.size() > 1 && it == list[-1]) {
|
|
label = "and ${label}"
|
|
}
|
|
label
|
|
}.join(", ")
|
|
}
|
|
|
|
return fancify(listOfStrings)
|
|
}
|
|
|
|
def schedulingHrefDescription() {
|
|
if (startDateTime() || endDateTime()) {
|
|
calendarHrefDescription()
|
|
} else {
|
|
def descriptionParts = []
|
|
if (days) {
|
|
descriptionParts << "On ${fancyString(days)},"
|
|
}
|
|
|
|
descriptionParts << "${fancyDeviceString(theLocks)} will be accessible"
|
|
if ((andOrTime != null) || (modeStart == null)) {
|
|
if (startTime) {
|
|
descriptionParts << "at ${humanReadableStartDate()}"
|
|
}
|
|
if (endTime) {
|
|
descriptionParts << "until ${humanReadableEndDate()}"
|
|
}
|
|
}
|
|
|
|
if (modeStart) {
|
|
if (startTime && andOrTime) {
|
|
descriptionParts << andOrTime
|
|
}
|
|
descriptionParts << "when ${location.name} enters '${modeStart}' mode"
|
|
}
|
|
|
|
if (descriptionParts.size() <= 1) {
|
|
// locks will be in the list no matter what. No rules are set if only locks are in the list
|
|
return null
|
|
}
|
|
return descriptionParts.join(" ")
|
|
}
|
|
}
|
|
|
|
def installed() {
|
|
log.debug "Installing 'Locks' with settings: ${settings}"
|
|
initialize()
|
|
}
|
|
|
|
def updated() {
|
|
log.debug "Updating 'Locks' with settings: ${settings}"
|
|
initialize()
|
|
}
|
|
|
|
private initialize() {
|
|
unsubscribe()
|
|
unschedule()
|
|
if (startTime && !startDateTime()) {
|
|
log.debug "scheduling access routine to run at ${startTime}"
|
|
schedule(startTime, "reconcileCodesStart")
|
|
} else if (startDateTime()) {
|
|
// There's a start date, so let's run then
|
|
log.debug "scheduling RUNONCE start"
|
|
runOnce(startDateTime().format(smartThingsDateFormat(), location.timeZone), "reconcileCodesStart")
|
|
}
|
|
|
|
if (endTime && !endDateTime()) {
|
|
log.debug "scheduling access denial routine to run at ${endTime}"
|
|
schedule(endTime, "reconcileCodesEnd")
|
|
} else if (endDateTime()) {
|
|
// There's a end date, so let's run then
|
|
log.debug "scheduling RUNONCE end"
|
|
runOnce(endDateTime().format(smartThingsDateFormat(), location.timeZone), "reconcileCodesEnd")
|
|
}
|
|
|
|
subscribe(location, locationHandler)
|
|
|
|
subscribe(theLocks, "codeReport", codereturn)
|
|
subscribe(theLocks, "lock", codeUsed)
|
|
subscribe(theLocks, "reportAllCodes", pollCodeReport, [filterEvents:false])
|
|
if (keypad) {
|
|
subscribe(location,"alarmSystemStatus",alarmStatusHandler)
|
|
subscribe(keypad,"codeEntered",codeEntryHandler)
|
|
}
|
|
|
|
revokeDisabledUsers()
|
|
reconcileCodes()
|
|
lockErrorLoopReset()
|
|
initalizeLockData()
|
|
|
|
log.debug "state: ${state}"
|
|
}
|
|
|
|
def resetAllCodeUsage() {
|
|
for (int i = 1; i <= settings.maxUsers; i++) {
|
|
lockErrorLoopReset()
|
|
resetCodeUsage(i)
|
|
}
|
|
log.debug "reseting all code usage"
|
|
}
|
|
|
|
def resetCodeUsage(i) {
|
|
if(state."userState${i}" == null) {
|
|
state."userState${i}" = [:]
|
|
state."userState${i}".enabled = true
|
|
}
|
|
state."userState${i}".usage = 0
|
|
}
|
|
|
|
def enableUser(i) {
|
|
state."userState${i}".enabled = true
|
|
}
|
|
|
|
def initalizeLockData() {
|
|
theLocks.each { lock->
|
|
if (state."lock${lock.id}" == null) {
|
|
state."lock${lock.id}" = [:]
|
|
}
|
|
}
|
|
}
|
|
|
|
def lockErrorLoopReset() {
|
|
state.error_loop_count = 0
|
|
theLocks.each { lock->
|
|
if (state."lock${lock.id}" == null) {
|
|
state."lock${lock.id}" = [:]
|
|
}
|
|
state."lock${lock.id}".error_loop = false
|
|
}
|
|
}
|
|
|
|
|
|
def locationHandler(evt) {
|
|
log.debug "locationHandler evt: ${evt.value}"
|
|
if (modeStart) {
|
|
reconcileCodes()
|
|
}
|
|
}
|
|
|
|
def reconcileCodes() {
|
|
if (isAbleToStart()) {
|
|
grantAccess()
|
|
} else {
|
|
revokeAccess()
|
|
}
|
|
}
|
|
|
|
def reconcileCodesStart() {
|
|
// schedule start of reconcileCodes
|
|
reconcileCodes()
|
|
if (calStartPhrase) {
|
|
location.helloHome.execute(calStartPhrase)
|
|
}
|
|
}
|
|
|
|
def reconcileCodesEnd() {
|
|
// schedule end of reconcileCodes
|
|
reconcileCodes()
|
|
if (calEndPhrase) {
|
|
location.helloHome.execute(calEndPhrase)
|
|
}
|
|
}
|
|
|
|
def isAbleToStart() {
|
|
def dateStart = startDateTime()
|
|
def dateEnd = endDateTime()
|
|
|
|
if (dateStart || dateEnd) {
|
|
// calendar schedule above all
|
|
return checkCalendarSchedule(dateStart, dateEnd)
|
|
} else if (modeStart || startTime || endTime || days) {
|
|
// No calendar set, check daily schedule
|
|
if (isCorrectDay()) {
|
|
// it's the right day
|
|
checkDailySchedule()
|
|
} else {
|
|
// it's the wrong day
|
|
return false
|
|
}
|
|
} else {
|
|
// no schedule
|
|
return true
|
|
}
|
|
}
|
|
|
|
def checkDailySchedule() {
|
|
if (andOrTime && modeStart && (isCorrectMode() || isInScheduledTime())) {
|
|
// in correct mode or time with and/or switch
|
|
if (andOrTime == 'and') {
|
|
// must be both
|
|
if (isCorrectMode() && isInScheduledTime()) {
|
|
// is both
|
|
return true
|
|
} else {
|
|
// is not both
|
|
return false
|
|
}
|
|
} else {
|
|
// could be either
|
|
if (isCorrectMode() || isInScheduledTime()) {
|
|
// it is either mode or time
|
|
return true
|
|
} else {
|
|
// is not either mode or time
|
|
return false
|
|
}
|
|
}
|
|
} else {
|
|
// Allow either mode or time, no andOrTime is set
|
|
if (isCorrectMode() || isInScheduledTime()) {
|
|
// it is either mode or time
|
|
return true
|
|
} else {
|
|
// is not either mode or time
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
def checkCalendarSchedule(dateStart, dateEnd) {
|
|
def now = rightNow().getTime()
|
|
if (dateStart && !dateEnd) {
|
|
// There's a start time, but no end time. Allow access after start
|
|
if (dateStart.getTime() > now) {
|
|
// It's after the start time
|
|
return true
|
|
} else {
|
|
// It's before the start time
|
|
return false
|
|
}
|
|
|
|
} else if (dateEnd && !dateStart) {
|
|
// There's a end time, but no start time. Allow access until end
|
|
if (dateStart.getTime() > now) {
|
|
// It's after the start time
|
|
return true
|
|
} else {
|
|
// It's before the start time
|
|
return false
|
|
}
|
|
|
|
} else {
|
|
// There's both an end time, and a start time. Allow access between them.
|
|
if (dateStart.getTime() < now && dateEnd.getTime() > now) {
|
|
// It's in calendar times
|
|
return true
|
|
} else {
|
|
// It's not in calendar times
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
def isCorrectMode() {
|
|
if (modeStart) {
|
|
// mode check is on
|
|
if (location.mode == modeStart) {
|
|
// we're in the right one mode
|
|
return true
|
|
} else {
|
|
// we're in the wrong mode
|
|
return false
|
|
}
|
|
} else {
|
|
// mode check is off
|
|
return false
|
|
}
|
|
}
|
|
|
|
def isInScheduledTime() {
|
|
def now = new Date()
|
|
if (startTime && endTime) {
|
|
def start = timeToday(startTime)
|
|
def stop = timeToday(endTime)
|
|
|
|
// there's both start time and end time
|
|
if (start.before(now) && stop.after(now)){
|
|
// It's between the times
|
|
return true
|
|
} else {
|
|
// It's not between the times
|
|
return false
|
|
}
|
|
} else if (startTime && !endTime){
|
|
// there's a start time, but no end time
|
|
def start = timeToday(startTime)
|
|
if (start.before(now)) {
|
|
// it's after start time
|
|
return true
|
|
} else {
|
|
//it's before start time
|
|
return false
|
|
}
|
|
} else if (!startTime && endTime) {
|
|
// there's an end time but no start time
|
|
def stop = timeToday(endTime)
|
|
if (stop.after(now)) {
|
|
// it's still before end time
|
|
return true
|
|
} else {
|
|
// it's after end time
|
|
return false
|
|
}
|
|
} else {
|
|
// there are no times
|
|
return false
|
|
}
|
|
}
|
|
|
|
def startDateTime() {
|
|
if (startDay && startMonth && startYear && startTime) {
|
|
def time = new Date().parse(smartThingsDateFormat(), startTime).format("'T'HH:mm:ss.SSSZ", timeZone(startTime))
|
|
return Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", "${startYear}-${startMonth}-${startDay}${time}")
|
|
} else {
|
|
// Start Date Time not set
|
|
return false
|
|
}
|
|
}
|
|
|
|
def endDateTime() {
|
|
if (endDay && endMonth && endYear && endTime) {
|
|
def time = new Date().parse(smartThingsDateFormat(), endTime).format("'T'HH:mm:ss.SSSZ", timeZone(endTime))
|
|
return Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", "${endYear}-${endMonth}-${endDay}${time}")
|
|
} else {
|
|
// End Date Time not set
|
|
return false
|
|
}
|
|
}
|
|
|
|
def rightNow() {
|
|
def now = new Date().format("yyyy-MM-dd'T'HH:mm:ss.SSSZ", location.timeZone)
|
|
return Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", now)
|
|
}
|
|
|
|
def isCorrectDay() {
|
|
def today = new Date().format("EEEE", location.timeZone)
|
|
log.debug "today: ${today}, days: ${days}"
|
|
if (!days || days.contains(today)) {
|
|
// if no days, assume every day
|
|
return true
|
|
}
|
|
log.trace "should not allow access - Not correct Day"
|
|
return false
|
|
}
|
|
|
|
def userSlotArray() {
|
|
def array = []
|
|
for (int i = 1; i <= settings.maxUsers; i++) {
|
|
if (settings."userSlot${i}") {
|
|
array << settings."userSlot${i}".toInteger()
|
|
}
|
|
}
|
|
return array
|
|
}
|
|
|
|
def enabledUsersArray() {
|
|
def array = []
|
|
for (int i = 1; i <= settings.maxUsers; i++) {
|
|
if (userIsEnabled(i)) {
|
|
array << i
|
|
}
|
|
}
|
|
return array
|
|
}
|
|
def enabledUsersSlotArray() {
|
|
def array = []
|
|
for (int i = 1; i <= settings.maxUsers; i++) {
|
|
if (userIsEnabled(i)) {
|
|
def userSlot = settings."userSlot${i}"
|
|
array << userSlot.toInteger()
|
|
}
|
|
}
|
|
return array
|
|
}
|
|
|
|
def disabledUsersSlotArray() {
|
|
def array = []
|
|
for (int i = 1; i <= settings.maxUsers; i++) {
|
|
if (!userIsEnabled(i)) {
|
|
if (settings."userSlot${i}") {
|
|
array << settings."userSlot${i}".toInteger()
|
|
}
|
|
}
|
|
}
|
|
return array
|
|
}
|
|
|
|
def codereturn(evt) {
|
|
def codeNumber = evt.data.replaceAll("\\D+","")
|
|
def codeSlot = evt.value
|
|
if (notifyAccessEnd || notifyAccessStart) {
|
|
if (userSlotArray().contains(evt.integerValue.toInteger())) {
|
|
def userName = settings."userName${usedUserIndex(evt.integerValue)}"
|
|
if (codeNumber == "") {
|
|
if (notifyAccessEnd) {
|
|
def message = "${userName} no longer has access to ${evt.displayName}"
|
|
if (codeNumber.isNumber()) {
|
|
state."lock${evt.deviceId}".codes."slot${codeSlot}" = codeNumber
|
|
}
|
|
send(message)
|
|
}
|
|
} else {
|
|
if (notifyAccessStart) {
|
|
def message = "${userName} now has access to ${evt.displayName}"
|
|
if (codeNumber.isNumber()) {
|
|
state."lock${evt.deviceId}".codes."slot${codeSlot}" = codeNumber
|
|
}
|
|
send(message)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
def usedUserIndex(usedSlot) {
|
|
for (int i = 1; i <= settings.maxUsers; i++) {
|
|
if (settings."userSlot${i}" && settings."userSlot${i}".toInteger() == usedSlot.toInteger()) {
|
|
return i
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
def codeUsed(evt) {
|
|
// check the status of the lock, helpful for some schlage locks.
|
|
runIn(10, doPoll)
|
|
log.debug("codeUsed evt.value: " + evt.value + ". evt.data: " + evt.data)
|
|
def message = null
|
|
|
|
if(evt.value == "unlocked" && evt.data) {
|
|
def codeData = new JsonSlurper().parseText(evt.data)
|
|
if(codeData.usedCode && codeData.usedCode.isNumber() && userSlotArray().contains(codeData.usedCode.toInteger())) {
|
|
def usedIndex = usedUserIndex(codeData.usedCode).toInteger()
|
|
def unlockUserName = settings."userName${usedIndex}"
|
|
message = "${evt.displayName} was unlocked by ${unlockUserName}"
|
|
// increment usage
|
|
state."userState${usedIndex}".usage = state."userState${usedIndex}".usage + 1
|
|
if(settings."userHomePhrases${usedIndex}") {
|
|
// Specific User Hello Home
|
|
if (settings."userNoRunPresence${usedIndex}" && settings."userDoRunPresence${usedIndex}" == null) {
|
|
if (!anyoneHome(settings."userNoRunPresence${usedIndex}")) {
|
|
location.helloHome.execute(settings."userHomePhrases${usedIndex}")
|
|
}
|
|
} else if (settings."userDoRunPresence${usedIndex}" && settings."userNoRunPresence${usedIndex}" == null) {
|
|
if (anyoneHome(settings."userDoRunPresence${usedIndex}")) {
|
|
location.helloHome.execute(settings."userHomePhrases${usedIndex}")
|
|
}
|
|
} else if (settings."userDoRunPresence${usedIndex}" && settings."userNoRunPresence${usedIndex}") {
|
|
if (anyoneHome(settings."userDoRunPresence${usedIndex}") && !anyoneHome(settings."userNoRunPresence${usedIndex}")) {
|
|
location.helloHome.execute(settings."userHomePhrases${usedIndex}")
|
|
}
|
|
} else {
|
|
location.helloHome.execute(settings."userHomePhrases${usedIndex}")
|
|
}
|
|
}
|
|
if(settings."burnCode${usedIndex}") {
|
|
theLocks.deleteCode(codeData.usedCode)
|
|
runIn(60*2, doPoll)
|
|
message += ". Now burning code."
|
|
}
|
|
//Don't send notification if muted
|
|
if(settings."dontNotify${usedIndex}" == true) {
|
|
message = null
|
|
}
|
|
}
|
|
} else if(evt.value == "unlocked" && settings.notifyUnlock) {
|
|
message = "${evt.displayName} has been manually unlocked"
|
|
} else if(evt.value == "locked" && settings.notifyLock) {
|
|
message = "${evt.displayName} has been locked"
|
|
}
|
|
|
|
if (message) {
|
|
log.debug("Sending message: " + message)
|
|
send(message)
|
|
}
|
|
|
|
if (homePhrases) {
|
|
performActions(evt)
|
|
}
|
|
}
|
|
|
|
def performActions(evt) {
|
|
if(evt.value == "unlocked" && evt.data) {
|
|
def codeData = new JsonSlurper().parseText(evt.data)
|
|
if(enabledUsersArray().contains(codeData.usedCode) || isManualUnlock(codeData)) {
|
|
// Global Hello Home
|
|
if(location.currentMode != modeIgnore) {
|
|
if (noRunPresence && doRunPresence == null) {
|
|
if (!anyoneHome(noRunPresence)) {
|
|
location.helloHome.execute(homePhrases)
|
|
}
|
|
} else if (doRunPresence && noRunPresence == null) {
|
|
if (anyoneHome(doRunPresence)) {
|
|
location.helloHome.execute(homePhrases)
|
|
}
|
|
} else if (doRunPresence && noRunPresence) {
|
|
if (anyoneHome(doRunPresence) && !anyoneHome(noRunPresence)) {
|
|
location.helloHome.execute(homePhrases)
|
|
}
|
|
} else {
|
|
location.helloHome.execute(homePhrases)
|
|
}
|
|
} else {
|
|
def routineMessage = "Already in ${modeIgnore} mode, skipping execution of ${homePhrases} routine."
|
|
log.debug routineMessage
|
|
send(routineMessage)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
def revokeDisabledUsers() {
|
|
def array = []
|
|
disabledUsersSlotArray().each { slot ->
|
|
array << ["code${slot}", ""]
|
|
}
|
|
def json = new groovy.json.JsonBuilder(array).toString()
|
|
if (json != '[]') {
|
|
theLocks.updateCodes(json)
|
|
runIn(60*2, doPoll)
|
|
}
|
|
}
|
|
|
|
def doPoll() {
|
|
// this gets codes if custom device is installed
|
|
if (!allCodesDone()) {
|
|
state.error_loop_count = state.error_loop_count + 1
|
|
}
|
|
theLocks.poll()
|
|
}
|
|
|
|
def grantAccess() {
|
|
def array = []
|
|
enabledUsersArray().each { user->
|
|
def userSlot = settings."userSlot${user}"
|
|
if (settings."userCode${user}" != null) {
|
|
def newCode = settings."userCode${user}"
|
|
array << ["code${userSlot}", "${newCode}"]
|
|
} else {
|
|
array << ["code${userSlot}", ""]
|
|
}
|
|
}
|
|
def json = new groovy.json.JsonBuilder(array).toString()
|
|
if (json != '[]') {
|
|
theLocks.updateCodes(json)
|
|
runIn(60*2, doPoll)
|
|
}
|
|
}
|
|
|
|
def revokeAccess() {
|
|
def array = []
|
|
enabledUsersArray().each { user->
|
|
def userSlot = settings."userSlot${user}"
|
|
array << ["code${userSlot}", ""]
|
|
}
|
|
def json = new groovy.json.JsonBuilder(array).toString()
|
|
if (json != '[]') {
|
|
theLocks.updateCodes(json)
|
|
runIn(60*2, doPoll)
|
|
}
|
|
}
|
|
|
|
def isManualUnlock(codeData) {
|
|
// check to see if the user wants this
|
|
if (manualUnlock) {
|
|
// garyd9's device type returns 'manual'
|
|
if ((codeData.usedCode == "") || (codeData.usedCode == null) || (codeData.usedCode == 'manual')) {
|
|
// no code used on unlock!
|
|
return true
|
|
} else {
|
|
// probably a code we're not dealing with here
|
|
return false
|
|
}
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
def isActiveBurnCode(slot) {
|
|
if (settings."burnCode${slot}" && state."userState${slot}".usage > 0) {
|
|
return false
|
|
} else {
|
|
// not a burn code / not yet used
|
|
return true
|
|
}
|
|
}
|
|
|
|
def pollCodeReport(evt) {
|
|
def active = isAbleToStart()
|
|
def codeData = new JsonSlurper().parseText(evt.data)
|
|
def numberOfCodes = codeData.codes
|
|
def userSlots = userSlotArray()
|
|
|
|
def array = []
|
|
|
|
(1..maxUsers).each { user->
|
|
def slot = settings."userSlot${user}"
|
|
def code = codeData."code${slot}"
|
|
def correctCode = settings."userCode${user}"
|
|
if (active) {
|
|
if (userIsEnabled(user) && isActiveBurnCode(user)) {
|
|
if (code == settings."userCode${user}") {
|
|
// Code is Active, We should be active. Nothing to do
|
|
} else {
|
|
// Code is incorrect, We should be active.
|
|
array << ["code${slot}", settings."userCode${user}"]
|
|
}
|
|
} else {
|
|
if (code != '') {
|
|
// Code is set, user is disabled, We should be disabled.
|
|
array << ["code${slot}", ""]
|
|
} else {
|
|
// Code is not set, user is disabled. Nothing to do
|
|
}
|
|
}
|
|
} else {
|
|
if (code != '') {
|
|
// Code is set, We should be disabled.
|
|
array << ["code${slot}", ""]
|
|
} else {
|
|
// Code is not active, We should be disabled. Nothing to do
|
|
}
|
|
}
|
|
}
|
|
|
|
def currentLock = theLocks.find{it.id == evt.deviceId}
|
|
populateDiscovery(codeData, currentLock)
|
|
|
|
def json = new groovy.json.JsonBuilder(array).toString()
|
|
if (json != '[]') {
|
|
runIn(60*2, doPoll)
|
|
|
|
//Lock is in an error state
|
|
state."lock${currentLock.id}".error_loop = true
|
|
def error_number = state.error_loop_count + 1
|
|
if (error_number <= 10) {
|
|
log.debug "sendCodes fix is: ${json} Error: ${error_number}/10"
|
|
currentLock.updateCodes(json)
|
|
} else {
|
|
log.debug "kill fix is: ${json}"
|
|
currentLock.updateCodes(json)
|
|
json = new JsonSlurper().parseText(json)
|
|
def n = 0
|
|
json.each { code ->
|
|
n = code[0][4..-1].toInteger()
|
|
def usedIndex = usedUserIndex(n)
|
|
def name = settings."userName${usedIndex}"
|
|
if (state."userState${usedIndex}".enabled) {
|
|
state."userState${usedIndex}".enabled = false
|
|
state."userState${usedIndex}".disabledReason = "Controller failed to set code"
|
|
send("Controller failed to set code for ${name}")
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
state."lock${currentLock.id}".error_loop = false
|
|
if (allCodesDone) {
|
|
lockErrorLoopReset()
|
|
} else {
|
|
runIn(60, doPoll)
|
|
}
|
|
}
|
|
}
|
|
|
|
def allCodesDone() {
|
|
def i = 0
|
|
def codeComplete = true
|
|
theLocks.each { lock->
|
|
i++
|
|
if (state."lock${lock.id}".error_loop == true) {
|
|
codeComplete = false
|
|
}
|
|
}
|
|
return codeComplete
|
|
}
|
|
|
|
private anyoneHome(sensors) {
|
|
def result = false
|
|
if(sensors.findAll { it?.currentPresence == "present" }) {
|
|
result = true
|
|
}
|
|
result
|
|
}
|
|
|
|
private send(msg) {
|
|
if (notificationStartTime != null && notificationEndTime != null) {
|
|
def start = timeToday(notificationStartTime)
|
|
def stop = timeToday(notificationEndTime)
|
|
def now = new Date()
|
|
if (start.before(now) && stop.after(now)){
|
|
sendMessage(msg)
|
|
}
|
|
} else {
|
|
sendMessage(msg)
|
|
}
|
|
}
|
|
|
|
private sendMessage(msg) {
|
|
if (notification) {
|
|
sendPush(msg)
|
|
} else {
|
|
sendNotificationEvent(msg)
|
|
}
|
|
if (phone) {
|
|
if ( phone.indexOf(";") > 1){
|
|
def phones = phone.split(";")
|
|
for ( def i = 0; i < phones.size(); i++) {
|
|
sendSms(phones[i], msg)
|
|
}
|
|
}
|
|
else {
|
|
sendSms(phone, msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
def populateDiscovery(codeData, lock) {
|
|
def codes = [:]
|
|
def codeSlots = 30
|
|
if (codeData.codes) {
|
|
codeSlots = codeData.codes
|
|
}
|
|
(1..codeSlots).each { slot->
|
|
codes."slot${slot}" = codeData."code${slot}"
|
|
}
|
|
state."lock${lock.id}".codes = codes
|
|
}
|
|
|
|
private String getPIN() {
|
|
return settings.pin.value.toString().padLeft(4,'0')
|
|
}
|
|
|
|
def alarmStatusHandler(event) {
|
|
log.debug "Keypad manager caught alarm status change: "+event.value
|
|
if (event.value == "off"){
|
|
keypad?.setDisarmed()
|
|
}
|
|
else if (event.value == "away"){
|
|
keypad?.setArmedAway()
|
|
}
|
|
else if (event.value == "stay") {
|
|
keypad?.setArmedStay()
|
|
}
|
|
}
|
|
|
|
private sendSHMEvent(String shmState) {
|
|
def event = [
|
|
name:"alarmSystemStatus",
|
|
value: shmState,
|
|
displayed: true,
|
|
description: "System Status is ${shmState}"
|
|
]
|
|
log.debug "test ${event}"
|
|
sendLocationEvent(event)
|
|
}
|
|
|
|
private execRoutine(armMode) {
|
|
if (armMode == 'away') {
|
|
location.helloHome?.execute(settings.armRoutine)
|
|
} else if (armMode == 'stay') {
|
|
location.helloHome?.execute(settings.stayRoutine)
|
|
} else if (armMode == 'off') {
|
|
location.helloHome?.execute(settings.disarmRoutine)
|
|
}
|
|
}
|
|
|
|
def codeEntryHandler(evt) {
|
|
//do stuff
|
|
log.debug "Caught code entry event! ${evt.value.value}"
|
|
|
|
def codeEntered = evt.value as String
|
|
|
|
def data = evt.data as String
|
|
def armMode = ''
|
|
def currentarmMode = keypad.currentValue("armMode")
|
|
def changedMode = 0
|
|
|
|
if (data == '0') {
|
|
armMode = 'off'
|
|
}
|
|
else if (data == '3') {
|
|
armMode = 'away'
|
|
}
|
|
else if (data == '1') {
|
|
armMode = 'stay'
|
|
}
|
|
else if (data == '2') {
|
|
armMode = 'stay' //Currently no separate night mode for SHM, set to 'stay'
|
|
} else {
|
|
log.error "${app.label}: Unexpected arm mode sent by keypad!: "+data
|
|
return []
|
|
}
|
|
|
|
def i = settings.maxUsers
|
|
def message = " "
|
|
while (i > 0) {
|
|
log.debug "i =" + i
|
|
def correctCode = settings."userCode${i}" as String
|
|
|
|
if (codeEntered == correctCode) {
|
|
|
|
log.debug "User Enabled: " + state."userState${i}".enabled
|
|
|
|
if (state."userState${i}".enabled == true) {
|
|
log.debug "Correct PIN entered. Change SHM state to ${armMode}"
|
|
//log.debug "Delay: ${armDelay}"
|
|
//log.debug "Data: ${data}"
|
|
//log.debug "armMode: ${armMode}"
|
|
|
|
def unlockUserName = settings."userName${i}"
|
|
|
|
if (data == "0") {
|
|
//log.debug "sendDisarmCommand"
|
|
runIn(0, "sendDisarmCommand")
|
|
message = "${evt.displayName} was disarmed by ${unlockUserName}"
|
|
}
|
|
else if (data == "1") {
|
|
//log.debug "sendStayCommand"
|
|
runIn(armDelay, "sendStayCommand")
|
|
message = "${evt.displayName} was armed to 'Stay' by ${unlockUserName}"
|
|
}
|
|
else if (data == "2") {
|
|
//log.debug "sendNightCommand"
|
|
runIn(armDelay, "sendNightCommand")
|
|
message = "${evt.displayName} was armed to 'Night' by ${unlockUserName}"
|
|
}
|
|
else if (data == "3") {
|
|
//log.debug "sendArmCommand"
|
|
runIn(armDelay, "sendArmCommand")
|
|
message = "${evt.displayName} was armed to 'Away' by ${unlockUserName}"
|
|
}
|
|
|
|
if(settings."burnCode${i}") {
|
|
state."userState${i}".enabled = false
|
|
message += ". Now burning code."
|
|
}
|
|
|
|
log.debug "${message}"
|
|
//log.debug "Initial Usage Count:" + state."userState${i}".usage
|
|
state."userState${i}".usage = state."userState${i}".usage + 1
|
|
//log.debug "Final Usage Count:" + state."userState${i}".usage
|
|
send(message)
|
|
i = 0
|
|
} else if (state."userState${i}".enabled == false){
|
|
log.debug "PIN Disabled"
|
|
//Could also call acknowledgeArmRequest() with a parameter of 4 to report invalid code. Opportunity to simplify code?
|
|
//keypad.sendInvalidKeycodeResponse()
|
|
}
|
|
}
|
|
changedMode = 1
|
|
i--
|
|
}
|
|
if (changedMode == 1 && i == 0) {
|
|
def errorMsg = "Incorrect Code Entered: ${codeEntered}"
|
|
if (notifyIncorrectPin) {
|
|
log.debug "Incorrect PIN"
|
|
send(errorMsg)
|
|
}
|
|
//Could also call acknowledgeArmRequest() with a parameter of 4 to report invalid code. Opportunity to simplify code?
|
|
keypad.sendInvalidKeycodeResponse()
|
|
}
|
|
}
|
|
def sendArmCommand() {
|
|
log.debug "Sending Arm Command."
|
|
keypad.acknowledgeArmRequest(3)
|
|
sendSHMEvent("away")
|
|
execRoutine("away")
|
|
}
|
|
def sendDisarmCommand() {
|
|
log.debug "Sending Disarm Command."
|
|
keypad.acknowledgeArmRequest(0)
|
|
sendSHMEvent("off")
|
|
execRoutine("off")
|
|
}
|
|
def sendStayCommand() {
|
|
log.debug "Sending Stay Command."
|
|
keypad.acknowledgeArmRequest(1)
|
|
sendSHMEvent("stay")
|
|
execRoutine("stay")
|
|
}
|
|
def sendNightCommand() {
|
|
log.debug "Sending Night Command."
|
|
keypad.acknowledgeArmRequest(2)
|
|
sendSHMEvent("stay")
|
|
execRoutine("stay")
|
|
} |