Update turn-on-by-zip-code.groovy
[smartapps.git] / official / smart-security.groovy
1 /**
2  *  Copyright 2015 SmartThings
3  *
4  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  *  in compliance with the License. You may obtain a copy of the License at:
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
10  *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
11  *  for the specific language governing permissions and limitations under the License.
12  *
13  *  Smart Security
14  *
15  *  Author: SmartThings
16  *  Date: 2013-03-07
17  */
18 definition(
19     name: "Smart Security",
20     namespace: "smartthings",
21     author: "SmartThings",
22     description: "Alerts you when there are intruders but not when you just got up for a glass of water in the middle of the night",
23     category: "Safety & Security",
24     iconUrl: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-IsItSafe.png",
25     iconX2Url: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-IsItSafe@2x.png"
26 )
27
28 preferences {
29         section("Sensors detecting an intruder") {
30                 input "intrusionMotions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false
31                 input "intrusionContacts", "capability.contactSensor", title: "Contact Sensors", multiple: true, required: false
32         }
33         section("Sensors detecting residents") {
34                 input "residentMotions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false
35         }
36         section("Alarm settings and actions") {
37                 input "alarms", "capability.alarm", title: "Which Alarm(s)", multiple: true, required: false
38         input "silent", "enum", options: ["Yes","No"], title: "Silent alarm only (Yes/No)"
39                 input "seconds", "number", title: "Delay in seconds before siren sounds"
40                 input "lights", "capability.switch", title: "Flash these lights (optional)", multiple: true, required: false
41                 input "newMode", "mode", title: "Change to this mode (optional)", required: false
42         }
43         section("Notify others (optional)") {
44                 input "textMessage", "text", title: "Send this message", multiple: false, required: false
45         input("recipients", "contact", title: "Send notifications to") {
46             input "phone", "phone", title: "To this phone", multiple: false, required: false
47         }
48         }
49         section("Arm system when residents quiet for (default 3 minutes)") {
50                 input "residentsQuietThreshold", "number", title: "Time in minutes", required: false
51         }
52 }
53
54 def installed() {
55         log.debug "INSTALLED"
56         subscribeToEvents()
57         state.alarmActive = null
58 }
59
60 def updated() {
61         log.debug "UPDATED"
62         unsubscribe()
63         subscribeToEvents()
64         unschedule()
65         state.alarmActive = null
66         state.residentsAreUp = null
67         state.lastIntruderMotion = null
68         alarms?.off()
69 }
70
71 private subscribeToEvents()
72 {
73         subscribe intrusionMotions, "motion", intruderMotion
74         // subscribe residentMotions, "motion", residentMotion
75         subscribe intrusionContacts, "contact", contact
76         subscribe alarms, "alarm", alarm
77         subscribe(app, appTouch)
78 }
79
80 private residentsHaveBeenQuiet()
81 {
82         def threshold = ((residentsQuietThreshold != null && residentsQuietThreshold != "") ? residentsQuietThreshold : 3) * 60 * 1000
83         def result = true
84         def t0 = new Date(now() - threshold)
85         for (sensor in residentMotions) {
86                 def recentStates = sensor.statesSince("motion", t0)
87                 if (recentStates.find{it.value == "active"}) {
88                         result = false
89                         break
90                 }
91         }
92         log.debug "residentsHaveBeenQuiet: $result"
93         result
94 }
95
96 private intruderMotionInactive()
97 {
98         def result = true
99         for (sensor in intrusionMotions) {
100                 if (sensor.currentMotion == "active") {
101                         result = false
102                         break
103                 }
104         }
105         result
106 }
107
108 private isResidentMotionSensor(evt)
109 {
110         residentMotions?.find{it.id == evt.deviceId} != null
111 }
112
113 def appTouch(evt)
114 {
115         alarms?.off()
116         state.alarmActive = false
117 }
118
119 // Here to handle old subscriptions
120 def motion(evt)
121 {
122         if (isResidentMotionSensor(evt)) {
123                 log.debug "resident motion, $evt.name: $evt.value"
124                 residentMotion(evt)
125         }
126         else {
127                 log.debug "intruder motion, $evt.name: $evt.value"
128                 intruderMotion(evt)
129         }
130 }
131
132 def intruderMotion(evt)
133 {
134         if (evt.value == "active") {
135                 log.debug "motion by potential intruder, residentsAreUp: $state.residentsAreUp"
136                 if (!state.residentsAreUp) {
137                         log.trace "checking if residents have been quiet"
138                         if (residentsHaveBeenQuiet()) {
139                                 log.trace "calling startAlarmSequence"
140                                 startAlarmSequence()
141                         }
142                         else {
143                                 log.trace "calling disarmIntrusionDetection"
144                                 disarmIntrusionDetection()
145                         }
146                 }
147         }
148         state.lastIntruderMotion = now()
149 }
150
151 def residentMotion(evt)
152 {
153         // Don't think we need this any more
154         //if (evt.value == "inactive") {
155         //      if (state.residentsAreUp) {
156         //      startReArmSequence()
157         //    }
158         //}
159   unsubscribe(residentMotions)
160 }
161
162 def contact(evt)
163 {
164         if (evt.value == "open") {
165                 // TODO - check for residents being up?
166                 if (!state.residentsAreUp) {
167                         if (residentsHaveBeenQuiet()) {
168                                 startAlarmSequence()
169                         }
170                         else {
171                                 disarmIntrusionDetection()
172                         }
173                 }
174         }
175 }
176
177 def alarm(evt)
178 {
179         log.debug "$evt.name: $evt.value"
180         if (evt.value == "off") {
181                 alarms?.off()
182                 state.alarmActive = false
183         }
184 }
185
186 private disarmIntrusionDetection()
187 {
188         log.debug "residents are up, disarming intrusion detection"
189         state.residentsAreUp = true
190         scheduleReArmCheck()
191 }
192
193 private scheduleReArmCheck()
194 {
195         def cron = "0 * * * * ?"
196         schedule(cron, "checkForReArm")
197         log.debug "Starting re-arm check, cron: $cron"
198 }
199
200 def checkForReArm()
201 {
202         def threshold = ((residentsQuietThreshold != null && residentsQuietThreshold != "") ? residentsQuietThreshold : 3) * 60 * 1000
203         log.debug "checkForReArm: threshold is $threshold"
204         // check last intruder motion
205         def lastIntruderMotion = state.lastIntruderMotion
206         log.debug "checkForReArm: lastIntruderMotion=$lastIntruderMotion"
207         if (lastIntruderMotion != null)
208         {
209                 log.debug "checkForReArm, time since last intruder motion: ${now() - lastIntruderMotion}"
210                 if (now() - lastIntruderMotion > threshold) {
211                         log.debug "re-arming intrusion detection"
212                         state.residentsAreUp = false
213                         unschedule()
214                 }
215         }
216         else {
217                 log.warn "checkForReArm: lastIntruderMotion was null, unable to check for re-arming intrusion detection"
218         }
219 }
220
221 private startAlarmSequence()
222 {
223         if (state.alarmActive) {
224                 log.debug "alarm already active"
225         }
226         else {
227                 state.alarmActive = true
228                 log.debug "starting alarm sequence"
229
230                 sendPush("Potential intruder detected!")
231
232                 if (newMode) {
233                         setLocationMode(newMode)
234                 }
235
236                 if (silentAlarm()) {
237                         log.debug "Silent alarm only"
238                         alarms?.strobe()
239             if (location.contactBookEnabled) {
240                 sendNotificationToContacts(textMessage ?: "Potential intruder detected", recipients)
241             }
242             else {
243                 if (phone) {
244                     sendSms(phone, textMessage ?: "Potential intruder detected")
245                 }
246             }
247                 }
248                 else {
249                         def delayTime = seconds
250                         if (delayTime) {
251                                 alarms?.strobe()
252                                 runIn(delayTime, "soundSiren")
253                                 log.debug "Sounding siren in $delayTime seconds"
254                         }
255                         else {
256                                 soundSiren()
257                         }
258                 }
259
260                 if (lights) {
261                         flashLights(Math.min((seconds/2) as Integer, 10))
262                 }
263         }
264 }
265
266 def soundSiren()
267 {
268         if (state.alarmActive) {
269                 log.debug "Sounding siren"
270         if (location.contactBookEnabled) {
271             sendNotificationToContacts(textMessage ?: "Potential intruder detected", recipients)
272         }
273         else {
274             if (phone) {
275                 sendSms(phone, textMessage ?: "Potential intruder detected")
276             }
277         }
278                 alarms?.both()
279                 if (lights) {
280                         log.debug "continue flashing lights"
281                         continueFlashing()
282                 }
283         }
284         else {
285                 log.debug "alarm activation aborted"
286         }
287         unschedule("soundSiren") // Temporary work-around to scheduling bug
288 }
289
290 def continueFlashing()
291 {
292         unschedule()
293         if (state.alarmActive) {
294                 flashLights(10)
295                 schedule(util.cronExpression(now() + 10000), "continueFlashing")
296         }
297 }
298
299 private flashLights(numFlashes) {
300         def onFor = 1000
301         def offFor = 1000
302
303         log.debug "FLASHING $numFlashes times"
304         def delay = 1L
305         numFlashes.times {
306                 log.trace "Switch on after  $delay msec"
307                 lights?.on(delay: delay)
308                 delay += onFor
309                 log.trace "Switch off after $delay msec"
310                 lights?.off(delay: delay)
311                 delay += offFor
312         }
313 }
314
315 private silentAlarm()
316 {
317         silent?.toLowerCase() in ["yes","true","y"]
318 }