Skip to content

Instantly share code, notes, and snippets.

@chuckpearce
Created March 31, 2015 03:47
Show Gist options
  • Save chuckpearce/d9f0d4a6f34855b38607 to your computer and use it in GitHub Desktop.
Save chuckpearce/d9f0d4a6f34855b38607 to your computer and use it in GitHub Desktop.
/**
* User Lock Manager v3.4
*
* Copyright 2015 Erik Thayer
*
*/
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")
}
def rootPage() {
dynamicPage(name: "rootPage", title: "", install: true, uninstall: true) {
section("Which Locks?") {
input "locks","capability.lockCodes", title: "Select Locks", required: true, multiple: true, submitOnChange: true
}
if (locks) {
section {
href(name: "toSetupPage", title: "User Settings", page: "setupPage", description: setupPageDescription(), state: setupPageDescription() ? "complete" : "")
href(name: "toNotificationPage", page: "notificationPage", title: "Notification Settings", description: "", state: "")
href(name: "toSchedulingPage", page: "schedulingPage", title: "Schedule (optional)", description: schedulingHrefDescription(), state: schedulingHrefDescription() ? "complete" : "")
href(name: "toOnUnlockPage", page: "onUnlockPage", title: "Actions after Unlock")
}
section {
label(title: "Label this SmartApp", required: false, defaultValue: "")
}
}
}
}
def setupPage() {
dynamicPage(name:"setupPage", title:"User Settings") {
section("How many Users? (1-30)?") {
input name: "maxUsers", title: "Number of users", type: "number", multiple: false, refreshAfterSelection: true
href(name: "toResetAllCodeUsage", title: "Reset Code Usage", page: "resetAllCodeUsagePage", description: "Tap to reset")
}
section("Users") {
for (int i = 1; i <= settings.maxUsers; i++) {
href(name: "toUserPage", page: "userPage", params: [number: i], required: false, description: userHrefDescription(i), title: userHrefTitle(i), state: userPageState(i) )
}
}
}
}
def userPage(params) {
dynamicPage(name:"setupPage", title:"User Settings") {
def i = params.params.number
log.debug "The number is $i"
section("Code #${i}") {
input(name: "userName${i}", type: "text", title: "Name for User", required: true, defaultValue: settings."userName${i}")
input(name: "userCode${i}", type: "text", title: "Code (4 to 8 digits)", required: false, defaultValue: settings."userCode${i}")
input(name: "userSlot${i}", type: "number", title: "Slot (1 through 30)", required: true, defaultValue: preSlectedCode(i))
}
section {
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}")
}
section {
href(name: "toSetupPage", title: "Back To Users", page: "setupPage")
href(name: "toResetCodeUsage", 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: "phone", title: "Text This Number", description: "Phone number", required: false, submitOnChange: true)
input(name: "notification", type: "bool", title: "Send A Push Notification", description: "Notification", required: false, submitOnChange: true)
if (phone != null || notification) {
input(name: "notifyAccess", title: "on User Entry", 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."
}
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
}
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
}
}
}
def onUnlockPage() {
dynamicPage(name:"onUnlockPage", title:"Initiate Actions") {
section("Actions") {
def phrases = location.helloHome?.getPhrases()*.label
if (phrases) {
phrases.sort()
input name: "homePhrases", type: "enum", title: "Home Mode Phrase", multiple: true,required: false, options: phrases, refreshAfterSelection: true
if (homePhrases) {
input name: "manualUnlock", title: "Initiate phrase on manual unlock also?", type: "bool", defaultValue: false, refreshAfterSelection: true
}
}
}
}
}
def resetCodeUsagePage(params) {
// do reset
resetCodeUsage(params.params.number)
dynamicPage(name:"resetCodeUsagePage", title:"User Settings") {
def i = params.params.number
section {
paragraph "User code usage has been reset."
}
section("Code #${i}") {
input(name: "userName${i}", type: "text", title: "Name for User", required: true, defaultValue: settings."userName${i}")
input(name: "userCode${i}", type: "text", title: "Code (4 to 8 digits)", required: false, defaultValue: settings."userCode${i}")
input(name: "userSlot${i}", type: "number", title: "Slot (1 through 30)", required: true, defaultValue: preSlectedCode(i))
}
section {
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}")
}
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("How many Users? (1-30)?") {
input name: "maxUsers", title: "Number of users", type: "number", multiple: false, refreshAfterSelection: true
}
section("Users") {
for (int i = 1; i <= settings.maxUsers; i++) {
href(name: "toUserPage", page: "userPage", params: [number: i], required: false, description: userHrefDescription(i), title: userHrefTitle(i), state: userPageState(i) )
}
}
}
}
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 setupPageDescription(){
def parts = []
for (int i = 1; i <= settings.maxUsers; i++) {
parts << settings."userName${i}"
}
return fancyString(parts)
}
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) {
if (!state.codeUsage) {
state.codeUsage = [:]
}
if (!state.codeUsage["code${i}"]) {
state.codeUsage["code${i}"] = 0
}
def uc = settings."userCode${i}"
def us = settings."userSlot${i}"
def usage = state?.codeUsage["code${i}"]
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}" && settings."userEnabled${i}") {
if (settings."burnCode${i}") {
if (state.codeUsage."code${i}" > 0) {
return 'incomplete'
} else {
return 'complete'
}
} else {
return 'complete'
}
} else if (settings."userCode${i}" && !settings."userEnabled${i}") {
return 'incomplete'
} else {
return 'incomplete'
}
}
def fancyDeviceString(devices = []) {
fancyString(devices.collect { deviceLabel(it) })
}
def deviceLabel(device) {
return device.label ?: device.name
}
def fancyString(listOfStrings) {
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(locks)} 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(locks, "codeReport", codereturn)
subscribe(locks, "lock", codeUsed)
subscribe(locks, "reportAllCodes", pollCodeReport, [filterEvents:false])
if (homePhrases) {
subscribe(locks, "lock", performActions)
}
revokeDisabledUsers()
reconcileCodes()
}
def resetAllCodeUsage() {
state.codeUsage = [:]
for (int i = 1; i <= settings.maxUsers; i++) {
state.codeUsage."code${i}" = 0
}
}
def resetCodeUsage(i) {
state.codeUsage."code${i}" = 0
}
def locationHandler(evt) {
log.debug "locationHandler evt: ${evt.value}"
if (!modeStart) {
return
}
modeCheck()
}
def reconcileCodes() {
if (isAbleToStart()) {
grantAccess()
} else {
revokeAccess()
}
}
def reconcileCodesStart() {
// schedule start of reconcileCodes
reconcileCodes()
}
def reconcileCodesEnd() {
// schedule end of reconcileCodes
reconcileCodes()
}
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
}
} 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) {
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) {
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"
return false
}
def userSlotArray() {
def array = []
for (int i = 1; i <= settings.maxUsers; i++) {
array << settings."userSlot${i}"
}
return array
}
def enabledUsersArray() {
def array = []
for (int i = 1; i <= settings.maxUsers; i++) {
if (settings."userEnabled${i}" == true) {
array << i
}
}
return array
}
def enabledUsersSlotArray() {
def array = []
for (int i = 1; i <= settings.maxUsers; i++) {
if (settings."userEnabled${i}" == true) {
def userSlot = settings."userSlot${i}"
array << userSlot
}
}
return array
}
def disabledUsersSlotArray() {
def array = []
for (int i = 1; i <= settings.maxUsers; i++) {
if (settings."userEnabled${i}" != true) {
def userSlot = settings."userSlot${i}"
array << userSlot
}
}
return array
}
def codereturn(evt) {
def codeNumber = evt.data.replaceAll("\\D+","")
def codeSlot = evt.value
if (userSlotArray().contains(evt.integerValue)) {
def userName = settings."userName${usedUserSlot(evt.integerValue)}"
if (codeNumber == "") {
def message = "${userName} no longer has access to ${evt.displayName}"
send(message)
} else {
def message = "${userName} now has access to ${evt.displayName}"
send(message)
}
}
}
def usedUserSlot(usedSlot) {
def slot = ''
for (int i = 1; i <= settings.maxUsers; i++) {
if (settings."userSlot${i}" == usedSlot) {
return i
}
}
return slot
}
def codeUsed(evt) {
def codeData = new JsonSlurper().parseText(evt.data)
if(evt.value == "unlocked" && evt.data) {
codeData = new JsonSlurper().parseText(evt.data)
if(userSlotArray().contains(codeData.usedCode)) {
def usedSlot = usedUserSlot(codeData.usedCode)
def unlockUserName = settings."userName${usedSlot}"
def message = "${evt.displayName} was unlocked by ${unlockUserName}"
// increment usage
state.codeUsage["code${usedSlot}"] = state.codeUsage["code${usedSlot}"] + 1
if(settings."burnCode${usedSlot}") {
locks.deleteCode(codeData.usedCode)
runIn(60*2, doPoll)
message += ". Now burning code."
}
send(message)
}
}
}
def revokeDisabledUsers() {
def array = []
for (int i = 1; i <= settings.maxUsers; i++) {
if (settings."userEnabled${i}" != true) {
def userSlot = settings."userSlot${i}"
array << ["code${userSlot}", ""]
}
}
def json = new groovy.json.JsonBuilder(array).toString()
if (json != '[]') {
locks.updateCodes(json)
runIn(60*2, doPoll)
}
}
def doPoll() {
// this gets codes if custom device is installed
locks.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 != '[]') {
locks.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 != '[]') {
locks.updateCodes(json)
runIn(60*2, doPoll)
}
}
def performActions(evt) {
def message = ""
if(evt.value == "unlocked" && evt.data) {
def codeData = new JsonSlurper().parseText(evt.data)
if(enabledUsersArray().contains(codeData.usedCode) || isManualUnlock(codeData)) {
location.helloHome.execute(settings.homePhrases)
}
}
}
def isManualUnlock(codeData) {
// check to see if the user wants this
if (manualUnlock) {
if ((codeData.usedCode == "") || (codeData.usedCode == null)) {
// 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.codeUsage["code${slot}"] > 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..numberOfCodes).each { n->
def code = codeData."code${n}"
def slot = n
if (userSlots.contains(slot)) {
def usedSlot = usedUserSlot(slot)
if (active) {
if (settings."userEnabled${usedSlot}" && isActiveBurnCode(usedSlot)) {
if (code == settings."userCode${usedSlot}") {
// Code is Active, We should be active. Nothing to do
} else {
// Code is incorrect, We should be active.
array << ["code${slot}", settings."userCode${usedSlot}"]
}
} 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 json = new groovy.json.JsonBuilder(array).toString()
if (json != '[]') {
locks.each() { lock ->
if (lock.id == evt.deviceId) {
log.debug "sendCodes fix is: ${json}"
locks.updateCodes(json)
runIn(60*2, doPoll)
}
}
}
}
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)
}
if (phone) {
sendSms(phone, msg)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment