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 // input "settings.z_contact", "capability.contactSensor"
1109 // input "settings.z_motion", "capability.motionSensor"
1110 // input "settings.z_movement", "capability.accelerationSensor"
1111 // input "settings.z_smoke", "capability.smokeDetector"
1112 // input "settings.z_water", "capability.waterSensor"
1113 // input "settings.remotes", "capability.button"
1115 private def initZones() {
1122 sensorType: "panic",
1127 if (settings.z_contact) {
1128 settings.z_contact.each() {
1131 sensorType: "contact",
1132 zoneType: settings["type_${it.id}"] ?: "exterior",
1133 delay: settings["delay_${it.id}"]
1136 subscribe(settings.z_contact, "contact.open", onContact)
1139 if (settings.z_motion) {
1140 settings.z_motion.each() {
1143 sensorType: "motion",
1144 zoneType: settings["type_${it.id}"] ?: "interior",
1145 delay: settings["delay_${it.id}"]
1148 subscribe(settings.z_motion, "motion.active", onMotion)
1151 if (settings.z_movement) {
1152 settings.z_movement.each() {
1155 sensorType: "acceleration",
1156 zoneType: settings["type_${it.id}"] ?: "interior",
1157 delay: settings["delay_${it.id}"]
1160 subscribe(settings.z_movement, "acceleration.active", onMovement)
1163 if (settings.z_smoke) {
1164 settings.z_smoke.each() {
1167 sensorType: "smoke",
1168 zoneType: settings["type_${it.id}"] ?: "alert",
1169 delay: settings["delay_${it.id}"]
1172 subscribe(settings.z_smoke, "smoke.detected", onSmoke)
1173 subscribe(settings.z_smoke, "smoke.tested", onSmoke)
1174 subscribe(settings.z_smoke, "carbonMonoxide.detected", onSmoke)
1175 subscribe(settings.z_smoke, "carbonMonoxide.tested", onSmoke)
1178 if (settings.z_water) {
1179 settings.z_water.each() {
1182 sensorType: "water",
1183 zoneType: settings["type_${it.id}"] ?: "alert",
1184 delay: settings["delay_${it.id}"]
1187 subscribe(settings.z_water, "water.wet", onWater)
1190 state.zones.each() {
1191 def zoneType = it.zoneType
1193 if (zoneType == "alert") {
1195 } else if (zoneType == "exterior") {
1196 it.armed = state.armed
1197 } else if (zoneType == "interior") {
1198 it.armed = state.armed && !state.stay
1205 private def initButtons() {
1206 LOG("initButtons()")
1208 state.buttonActions = []
1209 if (settings.remotes) {
1210 if (settings.buttonArmAway) {
1211 def button = settings.buttonArmAway.toInteger()
1212 def event = settings.holdArmAway ? "held" : "pushed"
1213 state.buttonActions << [button:button, event:event, action:"armAway"]
1216 if (settings.buttonArmStay) {
1217 def button = settings.buttonArmStay.toInteger()
1218 def event = settings.holdArmStay ? "held" : "pushed"
1219 state.buttonActions << [button:button, event:event, action:"armStay"]
1222 if (settings.buttonDisarm) {
1223 def button = settings.buttonDisarm.toInteger()
1224 def event = settings.holdDisarm ? "held" : "pushed"
1225 state.buttonActions << [button:button, event:event, action:"disarm"]
1228 if (settings.buttonPanic) {
1229 def button = settings.buttonPanic.toInteger()
1230 def event = settings.holdPanic ? "held" : "pushed"
1231 state.buttonActions << [button:button, event:event, action:"panic"]
1234 if (state.buttonActions) {
1235 subscribe(settings.remotes, "button", onButtonEvent)
1240 private def initRestApi() {
1241 if (settings.restApiEnabled) {
1242 if (!state.accessToken) {
1243 def token = createAccessToken()
1244 LOG("Created new access token: ${token})")
1246 state.url = "https://graph.api.smartthings.com/api/smartapps/installations/${app.id}/"
1247 log.info "REST API enabled"
1250 log.info "REST API disabled"
1254 private def isRestApiEnabled() {
1255 return settings.restApiEnabled && state.accessToken
1258 def onContact(evt) { onZoneEvent(evt, "contact") }
1259 def onMotion(evt) { onZoneEvent(evt, "motion") }
1260 def onMovement(evt) { onZoneEvent(evt, "acceleration") }
1261 def onSmoke(evt) { onZoneEvent(evt, "smoke") }
1262 def onWater(evt) { onZoneEvent(evt, "water") }
1264 private def onZoneEvent(evt, sensorType) {
1265 LOG("onZoneEvent(${evt.displayName}, ${sensorType})")
1267 def zone = getZoneForDevice(evt.deviceId, sensorType)
1269 log.warn "Cannot find zone for device ${evt.deviceId}"
1274 state.alarms << evt.displayName
1275 if (zone.zoneType == "alert" || !zone.delay || (state.stay && settings.stayDelayOff)) {
1278 myRunIn(state.delay, activateAlarm)
1283 def onLocation(evt) {
1284 LOG("onLocation(${evt.value})")
1286 String mode = evt.value
1287 if (settings.awayModes?.contains(mode)) {
1289 } else if (settings.stayModes?.contains(mode)) {
1291 } else if (settings.disarmModes?.contains(mode)) {
1296 def onButtonEvent(evt) {
1297 LOG("onButtonEvent(${evt.displayName})")
1299 if (!state.buttonActions || !evt.data) {
1303 def slurper = new JsonSlurper()
1304 def data = slurper.parseText(evt.data)
1305 def button = data.buttonNumber?.toInteger()
1307 LOG("Button '${button}' was ${evt.value}.")
1308 def item = state.buttonActions.find {
1309 it.button == button && it.event == evt.value
1313 LOG("Executing '${item.action}' button action")
1322 if (!atomicState.armed || atomicState.stay) {
1330 if (!atomicState.armed || !atomicState.stay) {
1338 if (atomicState.armed) {
1340 state.zones.each() {
1341 if (it.zoneType != "alert") {
1353 state.alarms << "Panic"
1363 // Send notification
1364 def msg = "${location.name} is "
1367 msg += state.stay ? "STAY" : "AWAY"
1376 def exitDelayExpired() {
1377 LOG("exitDelayExpired()")
1379 def armed = atomicState.armed
1380 def stay = atomicState.stay
1382 log.warn "exitDelayExpired: unexpected state!"
1387 state.zones.each() {
1388 def zoneType = it.zoneType
1389 if (zoneType == "exterior" || (zoneType == "interior" && !stay)) {
1394 def msg = "${location.name}: all "
1398 msg += "zones are armed."
1403 private def armPanel(stay) {
1404 LOG("armPanel(${stay})")
1412 def armDelay = false
1413 state.zones.each() {
1414 def zoneType = it.zoneType
1415 if (zoneType == "exterior") {
1422 } else if (zoneType == "interior") {
1425 } else if (it.delay) {
1434 def delay = armDelay && !(stay && settings.stayDelayOff) ? atomicState.delay : 0
1436 myRunIn(delay, exitDelayExpired)
1439 def mode = stay ? "STAY" : "AWAY"
1440 def msg = "${location.name} "
1442 msg += "will arm ${mode} in ${state.delay} seconds."
1444 msg += "is ARMED ${mode}."
1451 // .../armaway REST API endpoint
1455 if (!isRestApiEnabled()) {
1456 log.error "REST API disabled"
1457 return httpError(403, "Access denied")
1460 if (settings.pincode && settings.armWithPin && (params.pincode != settings.pincode.toString())) {
1461 log.error "Invalid PIN code '${params.pincode}'"
1462 return httpError(403, "Access denied")
1469 // .../armstay REST API endpoint
1473 if (!isRestApiEnabled()) {
1474 log.error "REST API disabled"
1475 return httpError(403, "Access denied")
1478 if (settings.pincode && settings.armWithPin && (params.pincode != settings.pincode.toString())) {
1479 log.error "Invalid PIN code '${params.pincode}'"
1480 return httpError(403, "Access denied")
1487 // .../disarm REST API endpoint
1491 if (!isRestApiEnabled()) {
1492 log.error "REST API disabled"
1493 return httpError(403, "Access denied")
1496 if (settings.pincode && (params.pincode != settings.pincode.toString())) {
1497 log.error "Invalid PIN code '${params.pincode}'"
1498 return httpError(403, "Access denied")
1505 // .../panic REST API endpoint
1509 if (!isRestApiEnabled()) {
1510 log.error "REST API disabled"
1511 return httpError(403, "Access denied")
1518 // .../status REST API endpoint
1522 if (!isRestApiEnabled()) {
1523 log.error "REST API disabled"
1524 return httpError(403, "Access denied")
1528 status: state.armed ? (state.stay ? "armed stay" : "armed away") : "disarmed",
1529 alarms: state.alarms
1535 def activateAlarm() {
1536 LOG("activateAlarm()")
1538 if (state.alarms.size() == 0) {
1539 log.warn "activateAlarm: false alarm"
1543 switch (settings.sirenMode) {
1545 settings.alarms*.siren()
1549 settings.alarms*.strobe()
1553 settings.alarms*.both()
1557 // Only turn on those switches that are currently off
1558 def switchesOn = settings.switches?.findAll { it?.currentSwitch == "off" }
1559 LOG("switchesOn: ${switchesOn}")
1562 state.offSwitches = switchesOn.collect { it.id }
1565 settings.cameras*.take()
1567 if (settings.helloHomeAction) {
1568 log.info "Executing HelloHome action '${settings.helloHomeAction}'"
1569 location.helloHome.execute(settings.helloHomeAction)
1572 def msg = "Alarm at ${location.name}!"
1573 state.alarms.each() {
1583 private def notify(msg) {
1584 LOG("notify(${msg})")
1588 if (state.alarms.size()) {
1589 // Alarm notification
1590 if (settings.pushMessage) {
1593 sendNotificationEvent(msg)
1596 if (settings.smsAlarmPhone1 && settings.phone1) {
1597 sendSms(phone1, msg)
1600 if (settings.smsAlarmPhone2 && settings.phone2) {
1601 sendSms(phone2, msg)
1604 if (settings.smsAlarmPhone3 && settings.phone3) {
1605 sendSms(phone3, msg)
1608 if (settings.smsAlarmPhone4 && settings.phone4) {
1609 sendSms(phone4, msg)
1612 if (settings.pushbulletAlarm && settings.pushbullet) {
1613 settings.pushbullet*.push(location.name, msg)
1616 // Status change notification
1617 if (settings.pushStatusMessage) {
1620 sendNotificationEvent(msg)
1623 if (settings.smsStatusPhone1 && settings.phone1) {
1624 sendSms(phone1, msg)
1627 if (settings.smsStatusPhone2 && settings.phone2) {
1628 sendSms(phone2, msg)
1631 if (settings.smsStatusPhone3 && settings.phone3) {
1632 sendSms(phone3, msg)
1635 if (settings.smsStatusPhone4 && settings.phone4) {
1636 sendSms(phone4, msg)
1639 if (settings.pushbulletStatus && settings.pushbullet) {
1640 settings.pushbullet*.push(location.name, msg)
1645 private def notifyVoice() {
1646 LOG("notifyVoice()")
1648 if (!settings.audioPlayer) {
1653 if (state.alarms.size()) {
1654 // Alarm notification
1655 if (settings.speechOnAlarm) {
1656 phrase = settings.speechText ?: getStatusPhrase()
1659 // Status change notification
1660 if (settings.speechOnStatus) {
1663 phrase = settings.speechTextArmedStay ?: getStatusPhrase()
1665 phrase = settings.speechTextArmedAway ?: getStatusPhrase()
1668 phrase = settings.speechTextDisarmed ?: getStatusPhrase()
1674 settings.audioPlayer*.playText(phrase)
1678 private def history(String event, String description = "") {
1679 LOG("history(${event}, ${description})")
1681 def history = atomicState.history
1682 history << [time: now(), event: event, description: description]
1683 if (history.size() > 10) {
1684 history = history.sort{it.time}
1685 history = history[1..-1]
1688 LOG("history: ${history}")
1689 state.history = history
1692 private def getStatusPhrase() {
1693 LOG("getStatusPhrase()")
1696 if (state.alarms.size()) {
1697 phrase = "Alarm at ${location.name}!"
1698 state.alarms.each() {
1702 phrase = "${location.name} security is "
1704 def mode = state.stay ? "stay" : "away"
1705 phrase += "armed in ${mode} mode."
1707 phrase += "disarmed."
1714 private def getHelloHomeActions() {
1715 def actions = location.helloHome?.getPhrases().collect() { it.label }
1716 return actions.sort()
1719 private def getAlarmStatus() {
1722 if (atomicState.armed) {
1723 alarmStatus = "ARMED "
1724 alarmStatus += atomicState.stay ? "STAY" : "AWAY"
1726 alarmStatus = "DISARMED"
1732 private def getZoneStatus(device, sensorType) {
1734 def zone = getZoneForDevice(device.id, sensorType)
1739 def str = "${device.displayName}: ${zone.zoneType}, "
1740 str += zone.armed ? "armed, " : "disarmed, "
1741 str += device.currentValue(sensorType)
1746 private def getZoneForDevice(id, sensorType) {
1747 return state.zones.find() { it.deviceId == id && it.sensorType == sensorType }
1750 private def isZoneReady(device, sensorType) {
1753 switch (sensorType) {
1755 ready = "closed".equals(device.currentValue("contact"))
1759 ready = "inactive".equals(device.currentValue("motion"))
1762 case "acceleration":
1763 ready = "inactive".equals(device.currentValue("acceleration"))
1767 ready = "clear".equals(device.currentValue("smoke"))
1771 ready = "dry".equals(device.currentValue("water"))
1781 private def getDeviceById(id, sensorType) {
1782 switch (sensorType) {
1784 return settings.z_contact?.find() { it.id == id }
1787 return settings.z_motion?.find() { it.id == id }
1789 case "acceleration":
1790 return settings.z_movement?.find() { it.id == id }
1793 return settings.z_smoke?.find() { it.id == id }
1796 return settings.z_water?.find() { it.id == id }
1802 private def getNumZones() {
1805 numZones += settings.z_contact?.size() ?: 0
1806 numZones += settings.z_motion?.size() ?: 0
1807 numZones += settings.z_movement?.size() ?: 0
1808 numZones += settings.z_smoke?.size() ?: 0
1809 numZones += settings.z_water?.size() ?: 0
1814 private def myRunIn(delay_s, func) {
1816 def date = new Date(now() + (delay_s * 1000))
1818 LOG("scheduled '${func}' to run at ${date}")
1822 private def mySendPush(msg) {
1823 // sendPush can throw an exception
1831 private def getVersion() {
1835 private def textCopyright() {
1836 def text = "Copyright © 2014 Statusbits.com"
1839 private def textLicense() {
1841 "This program is free software: you can redistribute it and/or " +
1842 "modify it under the terms of the GNU General Public License as " +
1843 "published by the Free Software Foundation, either version 3 of " +
1844 "the License, or (at your option) any later version.\n\n" +
1845 "This program is distributed in the hope that it will be useful, " +
1846 "but WITHOUT ANY WARRANTY; without even the implied warranty of " +
1847 "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU " +
1848 "General Public License for more details.\n\n" +
1849 "You should have received a copy of the GNU General Public License " +
1850 "along with this program. If not, see <http://www.gnu.org/licenses/>."
1853 private def LOG(message) {
1857 private def STATE() {
1858 //log.trace "state: ${state}"