e2SHM
This is my version of the eNTRY and eXIT delay for SHM (e2SHM).
eSHM
- this app can run in multi-instances so multiple profiles can be created. For example, 60 seconds delay for the front door, and 90 second delay for the back door.
- Delay can be defined separately for armed/away and/or armed/stay
Requirements:
- one simulated contact
- one simulated pushbutton
those can be created in the IDE. If desired, a seperate simulated contact can be paired with each real contact for “coolness.”
This app uses the full features of the built-in SHM so there is no risk!
This is my first cut at it and I am sharing so I can learn from everyone input.
Have fun,
=======
eSHM version 2.1
- Add iris keypad support (single code for home version)
- Add appTouch for easier app identification in automation/smartapp screen
-
Delay exit and entry beep
- Add Alexa annoucements via simulated contacts
-
Revised delay loop timimg. RunOnce is used instead of runIn.
- Remove simulation pushbutton in pingpong timeloop (not needed since RunOnce is used)
-
The pingpong timeloop is used only for notification, avoiding "race condition"
- Remove icon selection (not needed)
- general code cleanup
/**
* e2SHM
*
* Version 2.1.0 3/20/19
*
* 3/10/19 Initial release
*
* 3/20/19 Add iris keypad support (single code for home version)
* Delay exit and entry beep
* Add Alexa annoucements via simulated contacts
* Revised delay loop timimg. RunOnce is used instead of runIn.
* Remove simulation pushbutton in pingpong timeloop (not needed since RunOnce is used)
* The pingpong timeloop is used only for notification, avoiding "race condition"
* Remove icon selection (not needed)
* general code cleanup
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
definition(
name: "eSHMv2.1",
namespace: "smartthings",
author: "4Dad",
description: "Allow entry/exit alarm delay to SHM",
category: "Safety & Security",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
singleInstance: false
)
import groovy.json.JsonSlurper
import groovy.json.JsonBuilder
preferences {
page(name: "pageOne")
page(name: "pageTwo")
page(name: "pageThree")
page(name: "pageFour")
}
def pageOne() {
dynamicPage(name: "pageOne", title: "e2SHM App Settings", uninstall: true) {
section {
input(name: "delaymode", type: "enum", title: "Select SHM Mode to Delay", options: ["Armed/Away", "Armed/Stay"], required: true, multiple: false)
input(name: "exitentryduration", type: "number", title: "SHM Delay Duration in seconds", required: true)
}
section ("e2SHM Delay contact sensor settings") {
input(name: "realdoorcontact", type: "capability.contactSensor", title: "Actual Door Contact", required: true, multiple: true)
input(name: "simulateddoorcontact", type: "capability.contactSensor", title: "Simulated Door Contact", required: true, multiple: false)
}
section {
href(name: "topageTwo", title: "Keypad option", page: "pageTwo")
}
}
}
def pageTwo() {
dynamicPage(name: "pageTwo", title: "Keypad option", uninstall: true) {
section {
input(name: "keypad", type: "capability.tone", title: "Select keypad", required: false, multiple: true, refreshAfterSelection: true, submitOnChange: true)
}
if (keypad != null) {
section("Keypad user code setting") {
input(name: "userCode", type: "number", title: "Select 4 digits pincode", required: true, refreshAfterSelection: true)
input(name: "userSlot", type: "number", title: "Use default or... Enter slot (1 through 30)", required: false, defaultValue: getRandomNumberInRange())
}
}
section {
href(name: "topageThree", title: "Notification option", page: "pageThree")
}
}
}
def pageThree() {
dynamicPage(name: "pageThree", title: "Push notification option...", install: true, uninstall: true) {
section {
input(name: "eSHMPushMessage", type: "bool", title: "eSHM progress notification", required:false, defaultValue: false)
}
if (keypad != null) {
section {
input(name: "keypadPushMessage", type: "bool", title: "Keypad event notification", required:false, defaultValue: false)
input(name: "keypadDelayBeep", type: "bool", title: "Keypad entry and exit beep", required:false, defaultValue: false)
}
}
section {
input(name: "AlexaNotification", type: "bool", title: "Alexa says eSHM notification", required: false, defaultValue: false, submitOnChange: true)
}
if (AlexaNotification) {
section {
input(name: "AlexaSayEntry", type: "capability.contactSensor", title: "Alexa Entry virtual contact", required: true, multiple: false)
input(name: "AlexaSayExit", type: "capability.contactSensor", title: "Alexa Exit virtual contact", required: true, multiple: false)
}
}
section([mobileOnly:true]) {
label title: "Assign a name", required: false
}
section {
href(name: "topageFour", title: "To app info page", page: "pageFour")
href(name: "topageOne", title: "Back to start page", page: "pageOne")
}
}
}
def pageFour() {
dynamicPage(name: "pageFour", title: "Information", install: true, uninstall: true) {
section ("About this App") {
paragraph "${textAppName()}\n${textVersion()}\n${textCopyright()}\n\n${textLicense()}\n"
}
section ("Help & Instructions") {
paragraph "${textHelp()}"
}
}
}
def installed() {
initialize()
}
def updated() {
unsubscribe()
initialize()
}
def initialize() {
subscribe(app, appTouch)
// keypad initialization
if (keypad != null) {
subscribe(keypad, "tone", donothingHandler)
subscribe(keypad,"codeEntered", codeEntryHandler)
senduserCodetokeypad()
}
subscribe(location, "alarmSystemStatus", SHMStatusHandler)
subscribe(realdoorcontact, "contact.open", realcontactHandler)
subscribe(realdoorcontact, "contact.closed", realcontactHandler)
subscribe(simulateddoorcontact, "contact.open", donothingHandler)
subscribe(simulateddoorcontact, "contact.closed", donothingHandler)
// optional alexa says notification
if (AlexaNotification) {
subscribe(AlexaSayEntry, "contactSensor", donothingHandler)
subscribe(AlexaSayExit, "contactSensor", donothingHandler)
}
// App settings
atomicState.exitentrydelay = "off"
atomicState.pingpongloopis = 0
if (exitentryduration != 0) {
atomicState.alarmwaitstep = exitentryduration/10
} else {
atomicState.alarmwaitstep = 1
}
atomicState.alarmcounter = 1
atomicState.adoorwasopened = 0
// use the same SHM panel terms on smartthings app for input
if (delaymode == "Armed/Away") {
atomicState.delaySHMmode = "away"
} else {
atomicState.delaySHMmode = "stay"
}
// eSHM settings
simulateddoorcontact.close()
if (keypad != null) {
keypadSHMStatusHandler()
}
if (atomicState.delaySHMmode == location.currentState("alarmSystemStatus")?.value) {
if (atomicState.SHMalarmModeis != 1) {
if (eSHMPushMessage) {
sendPush ("SHM is already armed/${atomicState.delaySHMmode} mode")
}
atomicState.SHMalarmModeis = 1
atomicState.exitentrydelay = "entry"
}
}
}
// Send pincode to keypad(s)
def senduserCodetokeypad() {
def array = []
if (userCode != null) {
array << ["code${userSlot}", "${userCode}"]
} else {
array << ["code${userSlot}", "0000"]
if (keypadPushMessage) {
sendPush("Pincode reset to 0000 on ${keypad}")}
}
def json = new groovy.json.JsonBuilder(array).toString()
if (json != '[]') {
keypad.updateCodes(json)
}
}
// randomize slot for user code
// might be useful in future... but this is a cool way to assign slot
private int getRandomNumberInRange() {
def min = 1
def max = 30
Random r = new Random();
return r.nextInt((max - min) + 1) + min;
}
// keypad pincode handler
// verify pincode and set SHM mode
// SHM mode change either via keypad is handled by SHMStatusHandler
def codeEntryHandler(evt) {
def codeEntered = evt.value as String
def data = evt.data as String
def armMode = ''
switch (data) {
case "0" :
armMode = "off"
break
case "1" :
armMode = "stay"
break
case "2" :
armMode = "stay" // for night mode in DTH
break
case "3" :
armMode = "away"
break
default :
return []
break
}
def correctCode = userCode as String
if (codeEntered == correctCode) {
switch (data) {
case "0" :
sendLocationEvent(name: "alarmSystemStatus", value: "off")
keypad?.each() { it.setDisarmed() }
if (keypadPushMessage) { sendPush("SHM was disarmed with keypad") }
break
case "1" :
sendLocationEvent(name: "alarmSystemStatus", value: "stay")
if (keypadPushMessage) { sendPush("SHM was set to 'Armed/Stay' with keypad") }
break
case "2" :
sendLocationEvent(name: "alarmSystemStatus", value: "stay")
if (keypadPushMessage) { sendPush("SHM was set to 'Armed/Stay' with keypad") }
break
case "3" :
sendLocationEvent(name: "alarmSystemStatus", value: "away")
if (keypadPushMessage) { sendPush("SHM was set to 'Armed/Away' with keypad") }
break
default :
// nothing to do here
break
}
} else {
if (keypadPushMessage) { sendPush("Incorrect Code Entered: ${codeEntered}") }
}
}
def donothingHandler(evt) {
// nothing to do here
}
// select a keypad will activate this routine
// synch keypad indicator with SHM
// this routine catches SHM events even when eSHM is deactivated
def keypadSHMStatusHandler() {
def armmode = location.currentState("alarmSystemStatus")?.value
switch (armmode) {
case "stay" :
keypad?.each() { it.acknowledgeArmRequest(1) }
keypad?.each() { it.setArmedStay() }
break
case "away" :
keypad?.each() { it.acknowledgeArmRequest(3) }
keypad?.each() { it.setArmedAway() }
break
case "off" :
keypad?.each() { it.acknowledgeArmRequest(0) }
keypad?.each() { it.setDisarmed() }
break
default :
// nothing to do here
break
}
}
// Do SHM mode and exit alarm
// eSHM is activated when the SHM mode is the same as set eSHM mode
// deactivated all eSHM routines when SHM mode is not the same as eSHM mode
// deactivated means no action will be taken from subscribed events when they occurred
def SHMStatusHandler(evt) {
if (atomicState.delaySHMmode == location.currentState("alarmSystemStatus")?.value) {
atomicState.exitentrydelay = "exit"
atomicState.adoorwasopened = 0
atomicState.SHMalarmModeis = 1
atomicState.pingpongloopis = 1
atomicState.alarmcounter = 1
if (eSHMPushMessage) {
sendPush ("SHM is starting the armed/${atomicState.delaySHMmode} mode")
}
if (keypad != null) {
if (keypadDelayBeep) {
keypad?.each() { it.setExitDelay(exitentryduration) }
}
}
if (exitentryduration > 0) {
def date = new Date(now() + exitentryduration*1000)
runOnce(date, exitdelayalarmHandler)
def pingdata = true
pinpongtimeloopHandler(pingdata)
} else {
exitdelayalarmHandler()
}
} else {
atomicState.exitentrydelay = "off"
atomicState.adoorwasopened = 0
atomicState.pingpongloopis = 0
atomicState.SHMalarmModeis = 0
simulateddoorcontact.close()
if (eSHMPushMessage) {
sendPush ("eSHM is DEACTIVATED")
}
if (keypad != null) {
keypadSHMStatusHandler()
}
if (AlexaNotification) {
AlexaSayExit.close()
AlexaSayEntry.close()
}
}
}
// Monitor physical door contact for OPEN event
// Do SHM entry and exit conditions on selected door_contact
// only need one event to trigger the exit or entry handler
// - on entry: disarm SHM is the only option
// - on exit: door must be closed by the end of delay period to set alarm mode
def realcontactHandler(evt) {
if (atomicState.SHMalarmModeis == 1) {
if (evt.value == "open") {
if (atomicState.adoorwasopened == 0) {
if (atomicState.exitentrydelay == "exit") {
atomicState.adoorwasopened = 1
if (eSHMPushMessage) { sendPush("SHM Exit delay was triggered!") }
} else {
atomicState.exitentrydelay = "entry"
atomicState.pingpongloopis = 1
atomicState.alarmcounter = 1
atomicState.adoorwasopened = 1
if (eSHMPushMessage) { sendPush("SHM Entry delay was triggered!") }
if (keypad != null) {
if (keypadDelayBeep) {
keypad?.each() { it.setEntryDelay(exitentryduration) }
}
}
if (exitentryduration > 0) {
def date = new Date(now() + exitentryduration*1000)
runOnce(date, entrydelayalarmHandler)
def pingdata = true
pinpongtimeloopHandler(pingdata)
} else {
entrydelayalarmHandler()
}
}
}
}
}
}
// Do delay using a "pingpong loop" (current option)
// another option is to schedule the delay timeout and use this loop for push notification
// both options, a SHM mode change will stop this loop
def pinpongtimeloopHandler(pingdata) {
if (atomicState.pingpongloopis == 1) {
if (atomicState.alarmwaitstep > atomicState.alarmcounter) {
def timeleft = (atomicState.alarmwaitstep - atomicState.alarmcounter) * 10
switch (atomicState.exitentrydelay) {
case "exit" :
if (atomicState.alarmcounter % 2 == 0) {
if (eSHMPushMessage) {sendPush("SHM will be armed in ${timeleft} seconds") }
if (AlexaNotification) { AlexaSayExit.close() }
}
break
case "entry" :
if (atomicState.alarmcounter % 2 == 0) {
if (eSHMPushMessage) {sendPush("${timeleft} seconds left to DISARM SHM")}
if (AlexaNotification) { AlexaSayEntry.close() }
}
break
default :
// nothing to do here
break
}
atomicState.alarmcounter = atomicState.alarmcounter + 1
runIn(10, pingpongloop)
}
}
}
// pingpong loop back.
// SHM mode change will stop this loop
def pingpongloop() {
if (atomicState.SHMalarmModeis == 1) {
if (AlexaNotification) {
if (atomicState.exitentrydelay == "exit") {
AlexaSayExit.open() }
if (atomicState.exitentrydelay == "entry") {
AlexaSayEntry.open() }
}
def pingdata = true
pinpongtimeloopHandler(pingdata)
}
}
// Set SHM mode after exit delay duration expired: armed if door is closed; disarmed if door is opened
// SHM mode change during the delay period will stop this routine
// SHM mode change either via keypad or smartthings app is handled by SHMStatusHandler
def exitdelayalarmHandler() {
if (atomicState.SHMalarmModeis == 1 && atomicState.exitentrydelay == "exit") {
def open = realdoorcontact.findAll { it?.latestValue("contact") == "open" }
def list = open.size() > 1 ? "are" : "is"
atomicState.pingpongloopis = 0
atomicState.adoorwasopened = 0
if (open) {
if (eSHMPushMessage) {
sendPush("SHM activation was CANCELLED. ${open.join(', ')} ${list} open")
}
sendLocationEvent(name: "alarmSystemStatus", value: "off")
atomicState.exitentrydelay = "off"
} else {
if (eSHMPushMessage) {
sendPush("SHM was set to armed/${atomicState.delaySHMmode} mode")
}
atomicState.exitentrydelay = "entry"
}
keypadSHMStatusHandler()
if (AlexaNotification) {
AlexaSayExit.close()
AlexaSayEntry.close()
}
}
}
// Set SHM alarm condition after enter delay duration expired: alarm if SHM was not disarmed;
// SHM mode change during the delay period will stop this routine
// SHM mode change either via keypad or smartthings app is handled by SHMStatusHandler
def entrydelayalarmHandler () {
if (atomicState.SHMalarmModeis == 1 && atomicState.exitentrydelay == "entry") {
if (eSHMPushMessage) {
sendPush ("CAUTION..Intruder Alert...Intruder Alert...")
}
simulateddoorcontact.open()
atomicState.pingpongloopis = 0
atomicState.adoorwasopened = 0
}
if (AlexaNotification) {
AlexaSayExit.close()
AlexaSayEntry.close()
}
}
//Version/Copyright/Information/Help
private def textAppName() {
def text = "SHM delay for entry and exit"
}
private def textVersion() {
def text = "Version 2.1.0 3/20/19"
}
private def textCopyright() {
def text = "Copyright © 2019 illumatti"
}
private def textLicense() {
def text =
"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"+
"\n\n"+
" http://www.apache.org/licenses/LICENSE-2.0"+
"\n\n"+
"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."
}
private def textHelp() {
def text =
"e2SHM provides the missing delay alarm features in the built-in SHM.\n"+
"Requirements: \n"+
" . One simnulated contact to allow exit/entry delay to SHM. \n"+
"This contact is easy to make in the IDE/create new device/simulated devices. \n"+
"SHM settings: \n"+
"- Run this app in two instances: one for armed/away and one for armed/stay. \n"+
"- Remove door(s) need entry/exit delay from the SHM setting in each Armed/Stay"+
"and Armed/Away mode. \n"+
"- Add the simulated contact to the SHM settings for each mode. \n"+
"Options \n"+
"- Keypad with User code and entry and exit beep. (Tested with Iris keypad.) \n"+
"- Push notification on eSHM and keypad events \n"+
"- AlexaSays option for exit and entry /n"+
" . AlexaSays needs two simulated contacts. Use Alexa app to ask Alexa to do thing "+
"during the exit and entry duration. This app control those contacts and Alexa takes "+
"the set actions when contacts open in Alexa app."
}