2 * Smart Alarm is a multi-zone virtual alarm panel, featuring customizable
3 * security zones. Setting of an alarm can activate sirens, turn on light
4 * switches, push notification and text message. Alarm is armed and disarmed
5 * simply by setting SmartThings location 'mode'.
7 * Please visit <http://statusbits.github.io/smartalarm/> for more
10 * Version 2.4.3 (7/7/2015)
12 * The latest version of this file can be found on GitHub at:
13 * <https://github.com/statusbits/smartalarm/blob/master/SmartAlarm.groovy>
15 * --------------------------------------------------------------------------
17 * Copyright (c) 2014 Statusbits.com
19 * This program is free software: you can redistribute it and/or modify it
20 * under the terms of the GNU General Public License as published by the Free
21 * Software Foundation, either version 3 of the License, or (at your option)
24 * This program is distributed in the hope that it will be useful, but
25 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
26 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
29 * You should have received a copy of the GNU General Public License along
30 * with this program. If not, see <http://www.gnu.org/licenses/>.
33 import groovy.json.JsonSlurper
37 namespace: "statusbits",
38 author: "geko@statusbits.com",
39 description: '''A multi-zone virtual alarm panel, featuring customizable\
40 security zones. Setting of an alarm can activate sirens, turn on light\
41 switches, push notification and text message. Alarm is armed and disarmed\
42 simply by setting SmartThings location 'mode'.''',
43 category: "Safety & Security",
44 iconUrl: "http://statusbits.github.io/icons/SmartAlarm-128.png",
45 iconX2Url: "http://statusbits.github.io/icons/SmartAlarm-256.png",
46 oauth: [displayName:"Smart Alarm", displayLink:"http://statusbits.github.io/smartalarm/"]
52 page name:"pageUninstall"
53 page name:"pageStatus"
54 page name:"pageHistory"
55 page name:"pageSelectZones"
56 page name:"pageConfigureZones"
57 page name:"pageArmingOptions"
58 page name:"pageAlarmOptions"
59 page name:"pageNotifications"
60 page name:"pageRemoteOptions"
61 page name:"pageRestApiOptions"
66 action: [ GET: "apiArmAway" ]
69 path("/armaway/:pincode") {
70 action: [ GET: "apiArmAway" ]
74 action: [ GET: "apiArmStay" ]
77 path("/armstay/:pincode") {
78 action: [ GET: "apiArmStay" ]
82 action: [ GET: "apiDisarm" ]
85 path("/disarm/:pincode") {
86 action: [ GET: "apiDisarm" ]
90 action: [ GET: "apiPanic" ]
94 action: [ GET: "apiStatus" ]
102 if (state.version != getVersion()) {
103 return setupInit() ? pageAbout() : pageUninstall()
106 if (getNumZones() == 0) {
107 return pageSelectZones()
110 def alarmStatus = "Alarm is ${getAlarmStatus()}"
112 def pageProperties = [
117 uninstall: state.installed
120 return dynamicPage(pageProperties) {
122 if (state.zones.size() > 0) {
123 href "pageStatus", title:alarmStatus, description:"Tap for more information"
125 paragraph alarmStatus
127 if (state.history.size() > 0) {
128 href "pageHistory", title:"Event History", description:"Tap to view"
131 section("Setup Menu") {
132 href "pageSelectZones", title:"Add/Remove Zones", description:"Tap to open"
133 href "pageConfigureZones", title:"Configure Zones", description:"Tap to open"
134 href "pageArmingOptions", title:"Arming/Disarming Options", description:"Tap to open"
135 href "pageAlarmOptions", title:"Alarm Options", description:"Tap to open"
136 href "pageNotifications", title:"Notification Options", description:"Tap to open"
137 href "pageRemoteOptions", title:"Remote Control Options", description:"Tap to open"
138 href "pageRestApiOptions", title:"REST API Options", description:"Tap to open"
139 href "pageAbout", title:"About Smart Alarm", description:"Tap to open"
141 section([title:"Options", mobileOnly:true]) {
142 label title:"Assign a name", required:false
152 "Version ${getVersion()}\n${textCopyright()}\n\n" +
153 "You can contribute to the development of this app by making " +
154 "donation to geko@statusbits.com via PayPal."
157 url: "http://statusbits.github.io/smartalarm/",
159 title: "Tap here for more information...",
160 description:"http://statusbits.github.io/smartalarm/",
164 def pageProperties = [
167 nextPage: "pageSetup",
171 return dynamicPage(pageProperties) {
177 paragraph textLicense()
182 // Show "Uninstall" page
183 def pageUninstall() {
184 LOG("pageUninstall()")
187 "Smart Alarm version ${getVersion()} is not backward compatible " +
188 "with the currently installed version. Please uninstall the " +
189 "current version by tapping the Uninstall button below, then " +
190 "re-install Smart Alarm from the Dashboard. We are sorry for the " +
193 def pageProperties = [
194 name: "pageUninstall",
201 return dynamicPage(pageProperties) {
202 section("Uninstall Required") {
208 // Show "Status" page
212 def pageProperties = [
215 nextPage: "pageSetup",
219 return dynamicPage(pageProperties) {
221 paragraph "Alarm is ${getAlarmStatus()}"
224 if (settings.z_contact) {
225 section("Contact Sensors") {
226 settings.z_contact.each() {
227 def text = getZoneStatus(it, "contact")
235 if (settings.z_motion) {
236 section("Motion Sensors") {
237 settings.z_motion.each() {
238 def text = getZoneStatus(it, "motion")
246 if (settings.z_movement) {
247 section("Movement Sensors") {
248 settings.z_movement.each() {
249 def text = getZoneStatus(it, "acceleration")
257 if (settings.z_smoke) {
258 section("Smoke & CO Sensors") {
259 settings.z_smoke.each() {
260 def text = getZoneStatus(it, "smoke")
268 if (settings.z_water) {
269 section("Moisture Sensors") {
270 settings.z_water.each() {
271 def text = getZoneStatus(it, "water")
281 // Show "History" page
285 def pageProperties = [
287 //title: "Event History",
288 nextPage: "pageSetup",
292 def history = atomicState.history
294 return dynamicPage(pageProperties) {
295 section("Event History") {
296 if (history.size() == 0) {
297 paragraph "No history available."
299 paragraph "Not implemented"
305 // Show "Add/Remove Zones" page
306 def pageSelectZones() {
307 LOG("pageSelectZones()")
310 "A security zone is an area of your property protected by a sensor " +
311 "(contact, motion, movement, moisture or smoke)."
315 type: "capability.contactSensor",
316 title: "Which contact sensors?",
323 type: "capability.motionSensor",
324 title: "Which motion sensors?",
329 def inputMovement = [
331 type: "capability.accelerationSensor",
332 title: "Which movement sensors?",
339 type: "capability.smokeDetector",
340 title: "Which smoke & CO sensors?",
345 def inputMoisture = [
347 type: "capability.waterSensor",
348 title: "Which moisture sensors?",
353 def pageProperties = [
354 name: "pageSelectZones",
355 //title: "Add/Remove Zones",
356 nextPage: "pageConfigureZones",
360 return dynamicPage(pageProperties) {
361 section("Add/Remove Zones") {
372 // Show "Configure Zones" page
373 def pageConfigureZones() {
374 LOG("pageConfigureZones()")
377 "Security zones can be configured as either Exterior, Interior, " +
378 "Alert or Bypass. Exterior zones are armed in both Away and Stay " +
379 "modes, while Interior zones are armed only in Away mode, allowing " +
380 "you to move freely inside the premises while the alarm is armed " +
381 "in Stay mode. Alert zones are always armed and are typically used " +
382 "for smoke and flood alarms. Bypass zones are never armed. This " +
383 "allows you to temporarily exclude a zone from your security " +
385 "You can disable Entry and Exit Delays for individual zones."
387 def zoneTypes = ["exterior", "interior", "alert", "bypass"]
389 def pageProperties = [
390 name: "pageConfigureZones",
391 //title: "Configure Zones",
392 nextPage: "pageSetup",
396 return dynamicPage(pageProperties) {
397 section("Configure Zones") {
401 if (settings.z_contact) {
402 def devices = settings.z_contact.sort {it.displayName}
405 section("${it.displayName} (contact)") {
406 input "type_${devId}", "enum", title:"Zone Type", metadata:[values:zoneTypes], defaultValue:"exterior"
407 input "delay_${devId}", "bool", title:"Entry/Exit Delays", defaultValue:true
412 if (settings.z_motion) {
413 def devices = settings.z_motion.sort {it.displayName}
416 section("${it.displayName} (motion)") {
417 input "type_${devId}", "enum", title:"Zone Type", metadata:[values:zoneTypes], defaultValue:"interior"
418 input "delay_${devId}", "bool", title:"Entry/Exit Delays", defaultValue:false
423 if (settings.z_movement) {
424 def devices = settings.z_movement.sort {it.displayName}
427 section("${it.displayName} (movement)") {
428 input "type_${devId}", "enum", title:"Zone Type", metadata:[values:zoneTypes], defaultValue:"interior"
429 input "delay_${devId}", "bool", title:"Entry/Exit Delays", defaultValue:false
434 if (settings.z_smoke) {
435 def devices = settings.z_smoke.sort {it.displayName}
438 section("${it.displayName} (smoke)") {
439 input "type_${devId}", "enum", title:"Zone Type", metadata:[values:zoneTypes], defaultValue:"alert"
440 input "delay_${devId}", "bool", title:"Entry/Exit Delays", defaultValue:false
445 if (settings.z_water) {
446 def devices = settings.z_water.sort {it.displayName}
449 section("${it.displayName} (moisture)") {
450 input "type_${devId}", "enum", title:"Zone Type", metadata:[values:zoneTypes], defaultValue:"alert"
451 input "delay_${devId}", "bool", title:"Entry/Exit Delays", defaultValue:false
458 // Show "Arming/Disarming Options" page
459 def pageArmingOptions() {
460 LOG("pageArmingOptions()")
463 "Smart Alarm can be armed and disarmed by setting the home Mode. " +
464 "There are two arming modes - Stay and Away. Interior zones are " +
465 "not armed in Stay mode, allowing you to move freely inside your " +
469 "Exit and entry delay allows you to exit the premises after arming " +
470 "your alarm system and enter the premises while the alarm system " +
471 "is armed without setting off an alarm. You can optionally disable " +
472 "entry and exit delay when the alarm is armed in Stay mode."
474 def inputAwayModes = [
477 title: "Arm 'Away' in these Modes",
482 def inputStayModes = [
485 title: "Arm 'Stay' in these Modes",
490 def inputDisarmModes = [
493 title: "Disarm in these Modes",
501 metadata: [values:["30","45","60","90"]],
502 title: "Delay (in seconds)",
507 def inputDelayStay = [
508 name: "stayDelayOff",
510 title: "Disable delays in Stay mode",
515 def pageProperties = [
516 name: "pageArmingOptions",
517 //title: "Arming/Disarming Options",
518 nextPage: "pageSetup",
522 return dynamicPage(pageProperties) {
523 section("Arming/Disarming Options") {
530 input inputDisarmModes
533 section("Exit and Entry Delay") {
541 // Show "Alarm Options" page
542 def pageAlarmOptions() {
543 LOG("pageAlarmOptions()")
546 "You can configure Smart Alarm to take several actions when an " +
547 "alarm is set off, such as turning on sirens and light switches, " +
548 "taking camera snapshots and executing a 'Hello, Home' action."
552 type: "capability.alarm",
553 title: "Which sirens?",
558 def inputSirenMode = [
561 metadata: [values:["Off","Siren","Strobe","Both"]],
562 title: "Choose siren mode",
566 def inputSwitches = [
568 type: "capability.switch",
569 title: "Which switches?",
576 type: "capability.imageCapture",
577 title: "Which cameras?",
582 def hhActions = getHelloHomeActions()
583 def inputHelloHome = [
584 name: "helloHomeAction",
586 title: "Which 'Hello, Home' action?",
587 metadata: [values: hhActions],
591 def pageProperties = [
592 name: "pageAlarmOptions",
593 //title: "Alarm Options",
594 nextPage: "pageSetup",
598 return dynamicPage(pageProperties) {
599 section("Alarm Options") {
606 section("Switches") {
612 section("'Hello, Home' Actions") {
618 // Show "Notification Options" page
619 def pageNotifications() {
620 LOG("pageNotifications()")
623 "You can configure Smart Alarm to notify you when it is armed, " +
624 "disarmed or when an alarm is set off. Notifications can be send " +
625 "using either Push messages, SMS (text) messages and Pushbullet " +
626 "messaging service. Smart Alarm can also notify you with sounds or " +
627 "voice alerts using compatible audio devices, such as Sonos."
629 def inputPushAlarm = [
632 title: "Notify on Alarm",
636 def inputPushStatus = [
637 name: "pushStatusMessage",
639 title: "Notify on Status Change",
646 title: "Send to this number",
650 def inputPhone1Alarm = [
651 name: "smsAlarmPhone1",
653 title: "Notify on Alarm",
657 def inputPhone1Status = [
658 name: "smsStatusPhone1",
660 title: "Notify on Status Change",
667 title: "Send to this number",
671 def inputPhone2Alarm = [
672 name: "smsAlarmPhone2",
674 title: "Notify on Alarm",
678 def inputPhone2Status = [
679 name: "smsStatusPhone2",
681 title: "Notify on Status Change",
688 title: "Send to this number",
692 def inputPhone3Alarm = [
693 name: "smsAlarmPhone3",
695 title: "Notify on Alarm",
699 def inputPhone3Status = [
700 name: "smsStatusPhone3",
702 title: "Notify on Status Change",
709 title: "Send to this number",
713 def inputPhone4Alarm = [
714 name: "smsAlarmPhone4",
716 title: "Notify on Alarm",
720 def inputPhone4Status = [
721 name: "smsStatusPhone4",
723 title: "Notify on Status Change",
727 def inputPushbulletDevice = [
729 type: "device.pushbullet",
730 title: "Which Pushbullet devices?",
735 def inputPushbulletAlarm = [
736 name: "pushbulletAlarm",
738 title: "Notify on Alarm",
742 def inputPushbulletStatus = [
743 name: "pushbulletStatus",
745 title: "Notify on Status Change",
749 def inputAudioPlayers = [
751 type: "capability.musicPlayer",
752 title: "Which audio players?",
757 def inputSpeechOnAlarm = [
758 name: "speechOnAlarm",
760 title: "Notify on Alarm",
764 def inputSpeechOnStatus = [
765 name: "speechOnStatus",
767 title: "Notify on Status Change",
771 def inputSpeechTextAlarm = [
774 title: "Alarm Phrase",
778 def inputSpeechTextArmedAway = [
779 name: "speechTextArmedAway",
781 title: "Armed Away Phrase",
785 def inputSpeechTextArmedStay = [
786 name: "speechTextArmedStay",
788 title: "Armed Stay Phrase",
792 def inputSpeechTextDisarmed = [
793 name: "speechTextDisarmed",
795 title: "Disarmed Phrase",
799 def pageProperties = [
800 name: "pageNotifications",
801 //title: "Notification Options",
802 nextPage: "pageSetup",
806 return dynamicPage(pageProperties) {
807 section("Notification Options") {
810 section("Push Notifications") {
812 input inputPushStatus
814 section("Text Message (SMS) #1") {
816 input inputPhone1Alarm
817 input inputPhone1Status
819 section("Text Message (SMS) #2") {
821 input inputPhone2Alarm
822 input inputPhone2Status
824 section("Text Message (SMS) #3") {
826 input inputPhone3Alarm
827 input inputPhone3Status
829 section("Text Message (SMS) #4") {
831 input inputPhone4Alarm
832 input inputPhone4Status
834 section("Pushbullet Notifications") {
835 input inputPushbulletDevice
836 input inputPushbulletAlarm
837 input inputPushbulletStatus
839 section("Audio Notifications") {
840 input inputAudioPlayers
841 input inputSpeechOnAlarm
842 input inputSpeechOnStatus
843 input inputSpeechTextAlarm
844 input inputSpeechTextArmedAway
845 input inputSpeechTextArmedStay
846 input inputSpeechTextDisarmed
851 // Show "Remote Control Options" page
852 def pageRemoteOptions() {
853 LOG("pageRemoteOptions()")
856 "You can arm and disarm Smart Alarm using any compatible remote " +
857 "control, for example Aeon Labs Minimote."
861 type: "capability.button",
862 title: "Which remote controls?",
867 def inputArmAwayButton = [
868 name: "buttonArmAway",
870 title: "Which button?",
874 def inputArmAwayHold = [
877 title: "Hold to activate",
882 def inputArmStayButton = [
883 name: "buttonArmStay",
885 title: "Which button?",
889 def inputArmStayHold = [
892 title: "Hold to activate",
897 def inputDisarmButton = [
898 name: "buttonDisarm",
900 title: "Which button?",
904 def inputDisarmHold = [
907 title: "Hold to activate",
912 def inputPanicButton = [
915 title: "Which button?",
919 def inputPanicHold = [
922 title: "Hold to activate",
927 def pageProperties = [
928 name: "pageRemoteOptions",
929 //title: "Remote Control Options",
930 nextPage: "pageSetup",
934 return dynamicPage(pageProperties) {
935 section("Remote Control Options") {
940 section("Arm Away Button") {
941 input inputArmAwayButton
942 input inputArmAwayHold
945 section("Arm Stay Button") {
946 input inputArmStayButton
947 input inputArmStayHold
950 section("Disarm Button") {
951 input inputDisarmButton
952 input inputDisarmHold
955 section("Panic Button") {
956 input inputPanicButton
962 // Show "REST API Options" page
963 def pageRestApiOptions() {
964 LOG("pageRestApiOptions()")
967 "Smart Alarm can be controlled remotely by any Web client using " +
968 "REST API. Please refer to Smart Alarm documentation for more " +
972 "You can specify optional PIN code to protect arming and disarming " +
973 "Smart Alarm via REST API from unauthorized access. If set, the " +
974 "PIN code is always required for disarming Smart Alarm, however " +
975 "you can optionally turn it off for arming Smart Alarm."
978 name: "restApiEnabled",
980 title: "Enable REST API",
991 def inputArmWithPin = [
994 title: "Require PIN code to arm",
998 def pageProperties = [
999 name: "pageRestApiOptions",
1000 //title: "REST API Options",
1001 nextPage: "pageSetup",
1005 return dynamicPage(pageProperties) {
1006 section("REST API Options") {
1011 section("PIN Code") {
1012 paragraph textPincode
1014 input inputArmWithPin
1017 if (isRestApiEnabled()) {
1018 section("REST API Info") {
1019 paragraph "App ID:\n${app.id}"
1020 paragraph "Access Token:\n${state.accessToken}"
1030 state.installed = true
1041 private def setupInit() {
1044 if (state.installed == null) {
1045 state.installed = false
1051 def version = state.version as String
1052 if (version == null || version.startsWith('1')) {
1057 state.version = getVersion()
1061 private def initialize() {
1062 log.info "Smart Alarm. Version ${getVersion()}. ${textCopyright()}"
1063 LOG("settings: ${settings}")
1066 state.delay = settings.delay?.toInteger() ?: 30
1067 state.offSwitches = []
1070 if (settings.awayModes?.contains(location.mode)) {
1073 } else if (settings.stayModes?.contains(location.mode)) {
1084 subscribe(location, onLocation)
1089 private def clearAlarm() {
1093 settings.alarms*.off()
1095 // Turn off only those switches that we've turned on
1096 def switchesOff = state.offSwitches
1098 LOG("switchesOff: ${switchesOff}")
1099 settings.switches.each() {
1100 if (switchesOff.contains(it.id)) {
1104 state.offSwitches = []
1108 private def initZones() {
1115 sensorType: "panic",
1120 if (settings.z_contact) {
1121 settings.z_contact.each() {
1124 sensorType: "contact",
1125 zoneType: settings["type_${it.id}"] ?: "exterior",
1126 delay: settings["delay_${it.id}"]
1129 subscribe(settings.z_contact, "contact.open", onContact)
1132 if (settings.z_motion) {
1133 settings.z_motion.each() {
1136 sensorType: "motion",
1137 zoneType: settings["type_${it.id}"] ?: "interior",
1138 delay: settings["delay_${it.id}"]
1141 subscribe(settings.z_motion, "motion.active", onMotion)
1144 if (settings.z_movement) {
1145 settings.z_movement.each() {
1148 sensorType: "acceleration",
1149 zoneType: settings["type_${it.id}"] ?: "interior",
1150 delay: settings["delay_${it.id}"]
1153 subscribe(settings.z_movement, "acceleration.active", onMovement)
1156 if (settings.z_smoke) {
1157 settings.z_smoke.each() {
1160 sensorType: "smoke",
1161 zoneType: settings["type_${it.id}"] ?: "alert",
1162 delay: settings["delay_${it.id}"]
1165 subscribe(settings.z_smoke, "smoke.detected", onSmoke)
1166 subscribe(settings.z_smoke, "smoke.tested", onSmoke)
1167 subscribe(settings.z_smoke, "carbonMonoxide.detected", onSmoke)
1168 subscribe(settings.z_smoke, "carbonMonoxide.tested", onSmoke)
1171 if (settings.z_water) {
1172 settings.z_water.each() {
1175 sensorType: "water",
1176 zoneType: settings["type_${it.id}"] ?: "alert",
1177 delay: settings["delay_${it.id}"]
1180 subscribe(settings.z_water, "water.wet", onWater)
1183 state.zones.each() {
1184 def zoneType = it.zoneType
1186 if (zoneType == "alert") {
1188 } else if (zoneType == "exterior") {
1189 it.armed = state.armed
1190 } else if (zoneType == "interior") {
1191 it.armed = state.armed && !state.stay
1198 private def initButtons() {
1199 LOG("initButtons()")
1201 state.buttonActions = []
1202 if (settings.remotes) {
1203 if (settings.buttonArmAway) {
1204 def button = settings.buttonArmAway.toInteger()
1205 def event = settings.holdArmAway ? "held" : "pushed"
1206 state.buttonActions << [button:button, event:event, action:"armAway"]
1209 if (settings.buttonArmStay) {
1210 def button = settings.buttonArmStay.toInteger()
1211 def event = settings.holdArmStay ? "held" : "pushed"
1212 state.buttonActions << [button:button, event:event, action:"armStay"]
1215 if (settings.buttonDisarm) {
1216 def button = settings.buttonDisarm.toInteger()
1217 def event = settings.holdDisarm ? "held" : "pushed"
1218 state.buttonActions << [button:button, event:event, action:"disarm"]
1221 if (settings.buttonPanic) {
1222 def button = settings.buttonPanic.toInteger()
1223 def event = settings.holdPanic ? "held" : "pushed"
1224 state.buttonActions << [button:button, event:event, action:"panic"]
1227 if (state.buttonActions) {
1228 subscribe(settings.remotes, "button", onButtonEvent)
1233 private def initRestApi() {
1234 if (settings.restApiEnabled) {
1235 if (!state.accessToken) {
1236 def token = createAccessToken()
1237 LOG("Created new access token: ${token})")
1239 state.url = "https://graph.api.smartthings.com/api/smartapps/installations/${app.id}/"
1240 log.info "REST API enabled"
1243 log.info "REST API disabled"
1247 private def isRestApiEnabled() {
1248 return settings.restApiEnabled && state.accessToken
1251 def onContact(evt) { onZoneEvent(evt, "contact") }
1252 def onMotion(evt) { onZoneEvent(evt, "motion") }
1253 def onMovement(evt) { onZoneEvent(evt, "acceleration") }
1254 def onSmoke(evt) { onZoneEvent(evt, "smoke") }
1255 def onWater(evt) { onZoneEvent(evt, "water") }
1257 private def onZoneEvent(evt, sensorType) {
1258 LOG("onZoneEvent(${evt.displayName}, ${sensorType})")
1260 def zone = getZoneForDevice(evt.deviceId, sensorType)
1262 log.warn "Cannot find zone for device ${evt.deviceId}"
1267 state.alarms << evt.displayName
1268 if (zone.zoneType == "alert" || !zone.delay || (state.stay && settings.stayDelayOff)) {
1271 myRunIn(state.delay, activateAlarm)
1276 def onLocation(evt) {
1277 LOG("onLocation(${evt.value})")
1279 String mode = evt.value
1280 if (settings.awayModes?.contains(mode)) {
1282 } else if (settings.stayModes?.contains(mode)) {
1284 } else if (settings.disarmModes?.contains(mode)) {
1289 def onButtonEvent(evt) {
1290 LOG("onButtonEvent(${evt.displayName})")
1292 if (!state.buttonActions || !evt.data) {
1296 def slurper = new JsonSlurper()
1297 def data = slurper.parseText(evt.data)
1298 def button = data.buttonNumber?.toInteger()
1300 LOG("Button '${button}' was ${evt.value}.")
1301 def item = state.buttonActions.find {
1302 it.button == button && it.event == evt.value
1306 LOG("Executing '${item.action}' button action")
1315 if (!atomicState.armed || atomicState.stay) {
1323 if (!atomicState.armed || !atomicState.stay) {
1331 if (atomicState.armed) {
1333 state.zones.each() {
1334 if (it.zoneType != "alert") {
1346 state.alarms << "Panic"
1356 // Send notification
1357 def msg = "${location.name} is "
1360 msg += state.stay ? "STAY" : "AWAY"
1369 def exitDelayExpired() {
1370 LOG("exitDelayExpired()")
1372 def armed = atomicState.armed
1373 def stay = atomicState.stay
1375 log.warn "exitDelayExpired: unexpected state!"
1380 state.zones.each() {
1381 def zoneType = it.zoneType
1382 if (zoneType == "exterior" || (zoneType == "interior" && !stay)) {
1387 def msg = "${location.name}: all "
1391 msg += "zones are armed."
1396 private def armPanel(stay) {
1397 LOG("armPanel(${stay})")
1405 def armDelay = false
1406 state.zones.each() {
1407 def zoneType = it.zoneType
1408 if (zoneType == "exterior") {
1415 } else if (zoneType == "interior") {
1418 } else if (it.delay) {
1427 def delay = armDelay && !(stay && settings.stayDelayOff) ? atomicState.delay : 0
1429 myRunIn(delay, exitDelayExpired)
1432 def mode = stay ? "STAY" : "AWAY"
1433 def msg = "${location.name} "
1435 msg += "will arm ${mode} in ${state.delay} seconds."
1437 msg += "is ARMED ${mode}."
1444 // .../armaway REST API endpoint
1448 if (!isRestApiEnabled()) {
1449 log.error "REST API disabled"
1450 return httpError(403, "Access denied")
1453 if (settings.pincode && settings.armWithPin && (params.pincode != settings.pincode.toString())) {
1454 log.error "Invalid PIN code '${params.pincode}'"
1455 return httpError(403, "Access denied")
1462 // .../armstay REST API endpoint
1466 if (!isRestApiEnabled()) {
1467 log.error "REST API disabled"
1468 return httpError(403, "Access denied")
1471 if (settings.pincode && settings.armWithPin && (params.pincode != settings.pincode.toString())) {
1472 log.error "Invalid PIN code '${params.pincode}'"
1473 return httpError(403, "Access denied")
1480 // .../disarm REST API endpoint
1484 if (!isRestApiEnabled()) {
1485 log.error "REST API disabled"
1486 return httpError(403, "Access denied")
1489 if (settings.pincode && (params.pincode != settings.pincode.toString())) {
1490 log.error "Invalid PIN code '${params.pincode}'"
1491 return httpError(403, "Access denied")
1498 // .../panic REST API endpoint
1502 if (!isRestApiEnabled()) {
1503 log.error "REST API disabled"
1504 return httpError(403, "Access denied")
1511 // .../status REST API endpoint
1515 if (!isRestApiEnabled()) {
1516 log.error "REST API disabled"
1517 return httpError(403, "Access denied")
1521 status: state.armed ? (state.stay ? "armed stay" : "armed away") : "disarmed",
1522 alarms: state.alarms
1528 def activateAlarm() {
1529 LOG("activateAlarm()")
1531 if (state.alarms.size() == 0) {
1532 log.warn "activateAlarm: false alarm"
1536 switch (settings.sirenMode) {
1538 settings.alarms*.siren()
1542 settings.alarms*.strobe()
1546 settings.alarms*.both()
1550 // Only turn on those switches that are currently off
1551 def switchesOn = settings.switches?.findAll { it?.currentSwitch == "off" }
1552 LOG("switchesOn: ${switchesOn}")
1555 state.offSwitches = switchesOn.collect { it.id }
1558 settings.cameras*.take()
1560 if (settings.helloHomeAction) {
1561 log.info "Executing HelloHome action '${settings.helloHomeAction}'"
1562 location.helloHome.execute(settings.helloHomeAction)
1565 def msg = "Alarm at ${location.name}!"
1566 state.alarms.each() {
1576 private def notify(msg) {
1577 LOG("notify(${msg})")
1581 if (state.alarms.size()) {
1582 // Alarm notification
1583 if (settings.pushMessage) {
1586 sendNotificationEvent(msg)
1589 if (settings.smsAlarmPhone1 && settings.phone1) {
1590 sendSms(phone1, msg)
1593 if (settings.smsAlarmPhone2 && settings.phone2) {
1594 sendSms(phone2, msg)
1597 if (settings.smsAlarmPhone3 && settings.phone3) {
1598 sendSms(phone3, msg)
1601 if (settings.smsAlarmPhone4 && settings.phone4) {
1602 sendSms(phone4, msg)
1605 if (settings.pushbulletAlarm && settings.pushbullet) {
1606 settings.pushbullet*.push(location.name, msg)
1609 // Status change notification
1610 if (settings.pushStatusMessage) {
1613 sendNotificationEvent(msg)
1616 if (settings.smsStatusPhone1 && settings.phone1) {
1617 sendSms(phone1, msg)
1620 if (settings.smsStatusPhone2 && settings.phone2) {
1621 sendSms(phone2, msg)
1624 if (settings.smsStatusPhone3 && settings.phone3) {
1625 sendSms(phone3, msg)
1628 if (settings.smsStatusPhone4 && settings.phone4) {
1629 sendSms(phone4, msg)
1632 if (settings.pushbulletStatus && settings.pushbullet) {
1633 settings.pushbullet*.push(location.name, msg)
1638 private def notifyVoice() {
1639 LOG("notifyVoice()")
1641 if (!settings.audioPlayer) {
1646 if (state.alarms.size()) {
1647 // Alarm notification
1648 if (settings.speechOnAlarm) {
1649 phrase = settings.speechText ?: getStatusPhrase()
1652 // Status change notification
1653 if (settings.speechOnStatus) {
1656 phrase = settings.speechTextArmedStay ?: getStatusPhrase()
1658 phrase = settings.speechTextArmedAway ?: getStatusPhrase()
1661 phrase = settings.speechTextDisarmed ?: getStatusPhrase()
1667 settings.audioPlayer*.playText(phrase)
1671 private def history(String event, String description = "") {
1672 LOG("history(${event}, ${description})")
1674 def history = atomicState.history
1675 history << [time: now(), event: event, description: description]
1676 if (history.size() > 10) {
1677 history = history.sort{it.time}
1678 history = history[1..-1]
1681 LOG("history: ${history}")
1682 state.history = history
1685 private def getStatusPhrase() {
1686 LOG("getStatusPhrase()")
1689 if (state.alarms.size()) {
1690 phrase = "Alarm at ${location.name}!"
1691 state.alarms.each() {
1695 phrase = "${location.name} security is "
1697 def mode = state.stay ? "stay" : "away"
1698 phrase += "armed in ${mode} mode."
1700 phrase += "disarmed."
1707 private def getHelloHomeActions() {
1708 def actions = location.helloHome?.getPhrases().collect() { it.label }
1709 return actions.sort()
1712 private def getAlarmStatus() {
1715 if (atomicState.armed) {
1716 alarmStatus = "ARMED "
1717 alarmStatus += atomicState.stay ? "STAY" : "AWAY"
1719 alarmStatus = "DISARMED"
1725 private def getZoneStatus(device, sensorType) {
1727 def zone = getZoneForDevice(device.id, sensorType)
1732 def str = "${device.displayName}: ${zone.zoneType}, "
1733 str += zone.armed ? "armed, " : "disarmed, "
1734 str += device.currentValue(sensorType)
1739 private def getZoneForDevice(id, sensorType) {
1740 return state.zones.find() { it.deviceId == id && it.sensorType == sensorType }
1743 private def isZoneReady(device, sensorType) {
1746 switch (sensorType) {
1748 ready = "closed".equals(device.currentValue("contact"))
1752 ready = "inactive".equals(device.currentValue("motion"))
1755 case "acceleration":
1756 ready = "inactive".equals(device.currentValue("acceleration"))
1760 ready = "clear".equals(device.currentValue("smoke"))
1764 ready = "dry".equals(device.currentValue("water"))
1774 private def getDeviceById(id, sensorType) {
1775 switch (sensorType) {
1777 return settings.z_contact?.find() { it.id == id }
1780 return settings.z_motion?.find() { it.id == id }
1782 case "acceleration":
1783 return settings.z_movement?.find() { it.id == id }
1786 return settings.z_smoke?.find() { it.id == id }
1789 return settings.z_water?.find() { it.id == id }
1795 private def getNumZones() {
1798 numZones += settings.z_contact?.size() ?: 0
1799 numZones += settings.z_motion?.size() ?: 0
1800 numZones += settings.z_movement?.size() ?: 0
1801 numZones += settings.z_smoke?.size() ?: 0
1802 numZones += settings.z_water?.size() ?: 0
1807 private def myRunIn(delay_s, func) {
1809 def date = new Date(now() + (delay_s * 1000))
1811 LOG("scheduled '${func}' to run at ${date}")
1815 private def mySendPush(msg) {
1816 // sendPush can throw an exception
1824 private def getVersion() {
1828 private def textCopyright() {
1829 def text = "Copyright © 2014 Statusbits.com"
1832 private def textLicense() {
1834 "This program is free software: you can redistribute it and/or " +
1835 "modify it under the terms of the GNU General Public License as " +
1836 "published by the Free Software Foundation, either version 3 of " +
1837 "the License, or (at your option) any later version.\n\n" +
1838 "This program is distributed in the hope that it will be useful, " +
1839 "but WITHOUT ANY WARRANTY; without even the implied warranty of " +
1840 "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU " +
1841 "General Public License for more details.\n\n" +
1842 "You should have received a copy of the GNU General Public License " +
1843 "along with this program. If not, see <http://www.gnu.org/licenses/>."
1846 private def LOG(message) {
1850 private def STATE() {
1851 //log.trace "state: ${state}"