Update thermostat.groovy
[smartapps.git] / official / smart-alarm.groovy
1 /**
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'.
6  *
7  *  Please visit <http://statusbits.github.io/smartalarm/> for more
8  *  information.
9  *
10  *  Version 2.4.3 (7/7/2015)
11  *
12  *  The latest version of this file can be found on GitHub at:
13  *  <https://github.com/statusbits/smartalarm/blob/master/SmartAlarm.groovy>
14  *
15  *  --------------------------------------------------------------------------
16  *
17  *  Copyright (c) 2014 Statusbits.com
18  *
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)
22  *  any later version.
23  *
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
27  *  for more details.
28  *
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/>.
31  */
32
33 import groovy.json.JsonSlurper
34
35 definition(
36     name: "Smart Alarm",
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/"]
47 )
48
49 preferences {
50     page name:"pageSetup"
51     page name:"pageAbout"
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"
62 }
63
64 mappings {
65     path("/armaway") {
66         action: [ GET: "apiArmAway" ]
67     }
68
69     path("/armaway/:pincode") {
70         action: [ GET: "apiArmAway" ]
71     }
72
73     path("/armstay") {
74         action: [ GET: "apiArmStay" ]
75     }
76
77     path("/armstay/:pincode") {
78         action: [ GET: "apiArmStay" ]
79     }
80
81     path("/disarm") {
82         action: [ GET: "apiDisarm" ]
83     }
84
85     path("/disarm/:pincode") {
86         action: [ GET: "apiDisarm" ]
87     }
88
89     path("/panic") {
90         action: [ GET: "apiPanic" ]
91     }
92
93     path("/status") {
94         action: [ GET: "apiStatus" ]
95     }
96 }
97
98 // Show setup page
99 def pageSetup() {
100     LOG("pageSetup()")
101
102     if (state.version != getVersion()) {
103         return setupInit() ? pageAbout() : pageUninstall()
104     }
105
106     if (getNumZones() == 0) {
107         return pageSelectZones()
108     }
109
110     def alarmStatus = "Alarm is ${getAlarmStatus()}"
111
112     def pageProperties = [
113         name:       "pageSetup",
114         //title:      "Status",
115         nextPage:   null,
116         install:    true,
117         uninstall:  state.installed
118     ]
119
120     return dynamicPage(pageProperties) {
121         section("Status") {
122             if (state.zones.size() > 0) {
123                 href "pageStatus", title:alarmStatus, description:"Tap for more information"
124             } else {
125                 paragraph alarmStatus
126             }
127             if (state.history.size() > 0) {
128                 href "pageHistory", title:"Event History", description:"Tap to view"
129             }
130         }
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"
140         }
141         section([title:"Options", mobileOnly:true]) {
142             label title:"Assign a name", required:false
143         }
144     }
145 }
146
147 // Show "About" page
148 def pageAbout() {
149     LOG("pageAbout()")
150
151     def textAbout =
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."
155
156     def hrefInfo = [
157         url:        "http://statusbits.github.io/smartalarm/",
158         style:      "embedded",
159         title:      "Tap here for more information...",
160         description:"http://statusbits.github.io/smartalarm/",
161         required:   false
162     ]
163
164     def pageProperties = [
165         name:       "pageAbout",
166         //title:      "About",
167         nextPage:   "pageSetup",
168         uninstall:  false
169     ]
170
171     return dynamicPage(pageProperties) {
172         section("About") {
173             paragraph textAbout
174             href hrefInfo
175         }
176         section("License") {
177             paragraph textLicense()
178         }
179     }
180 }
181
182 // Show "Uninstall" page
183 def pageUninstall() {
184     LOG("pageUninstall()")
185
186     def text =
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 " +
191         "inconvenience."
192
193     def pageProperties = [
194         name:       "pageUninstall",
195         title:      "Warning!",
196         nextPage:   null,
197         uninstall:  true,
198         install:    false
199     ]
200
201     return dynamicPage(pageProperties) {
202         section("Uninstall Required") {
203             paragraph text
204         }
205     }
206 }
207
208 // Show "Status" page
209 def pageStatus() {
210     LOG("pageStatus()")
211
212     def pageProperties = [
213         name:       "pageStatus",
214         //title:      "Status",
215         nextPage:   "pageSetup",
216         uninstall:  false
217     ]
218
219     return dynamicPage(pageProperties) {
220         section("Status") {
221             paragraph "Alarm is ${getAlarmStatus()}"
222         }
223
224         if (settings.z_contact) {
225             section("Contact Sensors") {
226                 settings.z_contact.each() {
227                     def text = getZoneStatus(it, "contact")
228                     if (text) {
229                         paragraph text
230                     }
231                 }
232             }
233         }
234
235         if (settings.z_motion) {
236             section("Motion Sensors") {
237                 settings.z_motion.each() {
238                     def text = getZoneStatus(it, "motion")
239                     if (text) {
240                         paragraph text
241                     }
242                 }
243             }
244         }
245
246         if (settings.z_movement) {
247             section("Movement Sensors") {
248                 settings.z_movement.each() {
249                     def text = getZoneStatus(it, "acceleration")
250                     if (text) {
251                         paragraph text
252                     }
253                 }
254             }
255         }
256
257         if (settings.z_smoke) {
258             section("Smoke & CO Sensors") {
259                 settings.z_smoke.each() {
260                     def text = getZoneStatus(it, "smoke")
261                     if (text) {
262                         paragraph text
263                     }
264                 }
265             }
266         }
267
268         if (settings.z_water) {
269             section("Moisture Sensors") {
270                 settings.z_water.each() {
271                     def text = getZoneStatus(it, "water")
272                     if (text) {
273                         paragraph text
274                     }
275                 }
276             }
277         }
278     }
279 }
280
281 // Show "History" page
282 def pageHistory() {
283     LOG("pageHistory()")
284
285     def pageProperties = [
286         name:       "pageHistory",
287         //title:      "Event History",
288         nextPage:   "pageSetup",
289         uninstall:  false
290     ]
291
292     def history = atomicState.history
293
294     return dynamicPage(pageProperties) {
295         section("Event History") {
296             if (history.size() == 0) {
297                 paragraph "No history available."
298             } else {
299                 paragraph "Not implemented"
300             }
301         }
302     }
303 }
304
305 // Show "Add/Remove Zones" page
306 def pageSelectZones() {
307     LOG("pageSelectZones()")
308
309     def helpPage =
310         "A security zone is an area of your property protected by a sensor " +
311         "(contact, motion, movement, moisture or smoke)."
312
313     def inputContact = [
314         name:       "z_contact",
315         type:       "capability.contactSensor",
316         title:      "Which contact sensors?",
317         multiple:   true,
318         required:   false
319     ]
320
321     def inputMotion = [
322         name:       "z_motion",
323         type:       "capability.motionSensor",
324         title:      "Which motion sensors?",
325         multiple:   true,
326         required:   false
327     ]
328
329     def inputMovement = [
330         name:       "z_movement",
331         type:       "capability.accelerationSensor",
332         title:      "Which movement sensors?",
333         multiple:   true,
334         required:   false
335     ]
336
337     def inputSmoke = [
338         name:       "z_smoke",
339         type:       "capability.smokeDetector",
340         title:      "Which smoke & CO sensors?",
341         multiple:   true,
342         required:   false
343     ]
344
345     def inputMoisture = [
346         name:       "z_water",
347         type:       "capability.waterSensor",
348         title:      "Which moisture sensors?",
349         multiple:   true,
350         required:   false
351     ]
352
353     def pageProperties = [
354         name:       "pageSelectZones",
355         //title:      "Add/Remove Zones",
356         nextPage:   "pageConfigureZones",
357         uninstall:  false
358     ]
359
360     return dynamicPage(pageProperties) {
361         section("Add/Remove Zones") {
362             paragraph helpPage
363             input inputContact
364             input inputMotion
365             input inputMovement
366             input inputSmoke
367             input inputMoisture
368         }
369     }
370 }
371
372 // Show "Configure Zones" page
373 def pageConfigureZones() {
374     LOG("pageConfigureZones()")
375
376     def helpZones =
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 " +
384         "system.\n\n" +
385         "You can disable Entry and Exit Delays for individual zones."
386
387     def zoneTypes = ["exterior", "interior", "alert", "bypass"]
388
389     def pageProperties = [
390         name:       "pageConfigureZones",
391         //title:      "Configure Zones",
392         nextPage:   "pageSetup",
393         uninstall:  false
394     ]
395
396     return dynamicPage(pageProperties) {
397         section("Configure Zones") {
398             paragraph helpZones
399         }
400
401         if (settings.z_contact) {
402             def devices = settings.z_contact.sort {it.displayName}
403             devices.each() {
404                 def devId = it.id
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
408                 }
409             }
410         }
411
412         if (settings.z_motion) {
413             def devices = settings.z_motion.sort {it.displayName}
414             devices.each() {
415                 def devId = it.id
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
419                 }
420             }
421         }
422
423         if (settings.z_movement) {
424             def devices = settings.z_movement.sort {it.displayName}
425             devices.each() {
426                 def devId = it.id
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
430                 }
431             }
432         }
433
434         if (settings.z_smoke) {
435             def devices = settings.z_smoke.sort {it.displayName}
436             devices.each() {
437                 def devId = it.id
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
441                 }
442             }
443         }
444
445         if (settings.z_water) {
446             def devices = settings.z_water.sort {it.displayName}
447             devices.each() {
448                 def devId = it.id
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
452                 }
453             }
454         }
455     }
456 }
457
458 // Show "Arming/Disarming Options" page
459 def pageArmingOptions() {
460     LOG("pageArmingOptions()")
461
462     def helpArming =
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 " +
466         "home."
467
468     def helpDelay =
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."
473
474     def inputAwayModes = [
475         name:       "awayModes",
476         type:       "mode",
477         title:      "Arm 'Away' in these Modes",
478         multiple:   true,
479         required:   false
480     ]
481
482     def inputStayModes = [
483         name:       "stayModes",
484         type:       "mode",
485         title:      "Arm 'Stay' in these Modes",
486         multiple:   true,
487         required:   false
488     ]
489
490     def inputDisarmModes = [
491         name:       "disarmModes",
492         type:       "mode",
493         title:      "Disarm in these Modes",
494         multiple:   true,
495         required:   false
496     ]
497
498     def inputDelay = [
499         name:       "delay",
500         type:       "enum",
501         metadata:   [values:["30","45","60","90"]],
502         title:      "Delay (in seconds)",
503         defaultValue: "30",
504         required:   true
505     ]
506
507     def inputDelayStay = [
508         name:       "stayDelayOff",
509         type:       "bool",
510         title:      "Disable delays in Stay mode",
511         defaultValue: false,
512         required:   true
513     ]
514
515     def pageProperties = [
516         name:       "pageArmingOptions",
517         //title:      "Arming/Disarming Options",
518         nextPage:   "pageSetup",
519         uninstall:  false
520     ]
521
522     return dynamicPage(pageProperties) {
523         section("Arming/Disarming Options") {
524             paragraph helpArming
525         }
526
527         section("Modes") {
528             input inputAwayModes
529             input inputStayModes
530             input inputDisarmModes
531         }
532
533         section("Exit and Entry Delay") {
534             paragraph helpDelay
535             input inputDelay
536             input inputDelayStay
537         }
538     }
539 }
540
541 // Show "Alarm Options" page
542 def pageAlarmOptions() {
543     LOG("pageAlarmOptions()")
544
545     def helpAlarm =
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."
549
550     def inputAlarms = [
551         name:           "alarms",
552         type:           "capability.alarm",
553         title:          "Which sirens?",
554         multiple:       true,
555         required:       false
556     ]
557
558     def inputSirenMode = [
559         name:           "sirenMode",
560         type:           "enum",
561         metadata:       [values:["Off","Siren","Strobe","Both"]],
562         title:          "Choose siren mode",
563         defaultValue:   "Both"
564     ]
565
566     def inputSwitches = [
567         name:           "switches",
568         type:           "capability.switch",
569         title:          "Which switches?",
570         multiple:       true,
571         required:       false
572     ]
573
574     def inputCameras = [
575         name:           "cameras",
576         type:           "capability.imageCapture",
577         title:          "Which cameras?",
578         multiple:       true,
579         required:       false
580     ]
581
582     def hhActions = getHelloHomeActions()
583     def inputHelloHome = [
584         name:           "helloHomeAction",
585         type:           "enum",
586         title:          "Which 'Hello, Home' action?",
587         metadata:       [values: hhActions],
588         required:       false
589     ]
590
591     def pageProperties = [
592         name:       "pageAlarmOptions",
593         //title:      "Alarm Options",
594         nextPage:   "pageSetup",
595         uninstall:  false
596     ]
597
598     return dynamicPage(pageProperties) {
599         section("Alarm Options") {
600             paragraph helpAlarm
601         }
602         section("Sirens") {
603             input inputAlarms
604             input inputSirenMode
605         }
606         section("Switches") {
607             input inputSwitches
608         }
609         section("Cameras") {
610             input inputCameras
611         }
612         section("'Hello, Home' Actions") {
613             input inputHelloHome
614         }
615     }
616 }
617
618 // Show "Notification Options" page
619 def pageNotifications() {
620     LOG("pageNotifications()")
621
622     def helpAbout =
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."
628
629     def inputPushAlarm = [
630         name:           "pushMessage",
631         type:           "bool",
632         title:          "Notify on Alarm",
633         defaultValue:   true
634     ]
635
636     def inputPushStatus = [
637         name:           "pushStatusMessage",
638         type:           "bool",
639         title:          "Notify on Status Change",
640         defaultValue:   true
641     ]
642
643     def inputPhone1 = [
644         name:           "phone1",
645         type:           "phone",
646         title:          "Send to this number",
647         required:       false
648     ]
649
650     def inputPhone1Alarm = [
651         name:           "smsAlarmPhone1",
652         type:           "bool",
653         title:          "Notify on Alarm",
654         defaultValue:   false
655     ]
656
657     def inputPhone1Status = [
658         name:           "smsStatusPhone1",
659         type:           "bool",
660         title:          "Notify on Status Change",
661         defaultValue:   false
662     ]
663
664     def inputPhone2 = [
665         name:           "phone2",
666         type:           "phone",
667         title:          "Send to this number",
668         required:       false
669     ]
670
671     def inputPhone2Alarm = [
672         name:           "smsAlarmPhone2",
673         type:           "bool",
674         title:          "Notify on Alarm",
675         defaultValue:   false
676     ]
677
678     def inputPhone2Status = [
679         name:           "smsStatusPhone2",
680         type:           "bool",
681         title:          "Notify on Status Change",
682         defaultValue:   false
683     ]
684
685     def inputPhone3 = [
686         name:           "phone3",
687         type:           "phone",
688         title:          "Send to this number",
689         required:       false
690     ]
691
692     def inputPhone3Alarm = [
693         name:           "smsAlarmPhone3",
694         type:           "bool",
695         title:          "Notify on Alarm",
696         defaultValue:   false
697     ]
698
699     def inputPhone3Status = [
700         name:           "smsStatusPhone3",
701         type:           "bool",
702         title:          "Notify on Status Change",
703         defaultValue:   false
704     ]
705
706     def inputPhone4 = [
707         name:           "phone4",
708         type:           "phone",
709         title:          "Send to this number",
710         required:       false
711     ]
712
713     def inputPhone4Alarm = [
714         name:           "smsAlarmPhone4",
715         type:           "bool",
716         title:          "Notify on Alarm",
717         defaultValue:   false
718     ]
719
720     def inputPhone4Status = [
721         name:           "smsStatusPhone4",
722         type:           "bool",
723         title:          "Notify on Status Change",
724         defaultValue:   false
725     ]
726
727     def inputPushbulletDevice = [
728         name:           "pushbullet",
729         type:           "device.pushbullet",
730         title:          "Which Pushbullet devices?",
731         multiple:       true,
732         required:       false
733     ]
734
735     def inputPushbulletAlarm = [
736         name:           "pushbulletAlarm",
737         type:           "bool",
738         title:          "Notify on Alarm",
739         defaultValue:   true
740     ]
741
742     def inputPushbulletStatus = [
743         name:           "pushbulletStatus",
744         type:           "bool",
745         title:          "Notify on Status Change",
746         defaultValue:   true
747     ]
748
749     def inputAudioPlayers = [
750         name:           "audioPlayer",
751         type:           "capability.musicPlayer",
752         title:          "Which audio players?",
753         multiple:       true,
754         required:       false
755     ]
756
757     def inputSpeechOnAlarm = [
758         name:           "speechOnAlarm",
759         type:           "bool",
760         title:          "Notify on Alarm",
761         defaultValue:   true
762     ]
763
764     def inputSpeechOnStatus = [
765         name:           "speechOnStatus",
766         type:           "bool",
767         title:          "Notify on Status Change",
768         defaultValue:   true
769     ]
770
771     def inputSpeechTextAlarm = [
772         name:           "speechText",
773         type:           "text",
774         title:          "Alarm Phrase",
775         required:       false
776     ]
777
778     def inputSpeechTextArmedAway = [
779         name:           "speechTextArmedAway",
780         type:           "text",
781         title:          "Armed Away Phrase",
782         required:       false
783     ]
784
785     def inputSpeechTextArmedStay = [
786         name:           "speechTextArmedStay",
787         type:           "text",
788         title:          "Armed Stay Phrase",
789         required:       false
790     ]
791
792     def inputSpeechTextDisarmed = [
793         name:           "speechTextDisarmed",
794         type:           "text",
795         title:          "Disarmed Phrase",
796         required:       false
797     ]
798
799     def pageProperties = [
800         name:       "pageNotifications",
801         //title:      "Notification Options",
802         nextPage:   "pageSetup",
803         uninstall:  false
804     ]
805
806     return dynamicPage(pageProperties) {
807         section("Notification Options") {
808             paragraph helpAbout
809         }
810         section("Push Notifications") {
811             input inputPushAlarm
812             input inputPushStatus
813         }
814         section("Text Message (SMS) #1") {
815             input inputPhone1
816             input inputPhone1Alarm
817             input inputPhone1Status
818         }
819         section("Text Message (SMS) #2") {
820             input inputPhone2
821             input inputPhone2Alarm
822             input inputPhone2Status
823         }
824         section("Text Message (SMS) #3") {
825             input inputPhone3
826             input inputPhone3Alarm
827             input inputPhone3Status
828         }
829         section("Text Message (SMS) #4") {
830             input inputPhone4
831             input inputPhone4Alarm
832             input inputPhone4Status
833         }
834         section("Pushbullet Notifications") {
835             input inputPushbulletDevice
836             input inputPushbulletAlarm
837             input inputPushbulletStatus
838         }
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
847         }
848     }
849 }
850
851 // Show "Remote Control Options" page
852 def pageRemoteOptions() {
853     LOG("pageRemoteOptions()")
854
855     def helpRemote =
856         "You can arm and disarm Smart Alarm using any compatible remote " +
857         "control, for example Aeon Labs Minimote."
858
859     def inputRemotes = [
860         name:       "remotes",
861         type:       "capability.button",
862         title:      "Which remote controls?",
863         multiple:   true,
864         required:   false
865     ]
866
867     def inputArmAwayButton = [
868         name:       "buttonArmAway",
869         type:       "number",
870         title:      "Which button?",
871         required:   false
872     ]
873
874     def inputArmAwayHold = [
875         name:       "holdArmAway",
876         type:       "bool",
877         title:      "Hold to activate",
878         defaultValue: false,
879         required:   true
880     ]
881
882     def inputArmStayButton = [
883         name:       "buttonArmStay",
884         type:       "number",
885         title:      "Which button?",
886         required:    false
887     ]
888
889     def inputArmStayHold = [
890         name:       "holdArmStay",
891         type:       "bool",
892         title:      "Hold to activate",
893         defaultValue: false,
894         required:   true
895     ]
896
897     def inputDisarmButton = [
898         name:       "buttonDisarm",
899         type:       "number",
900         title:      "Which button?",
901         required:   false
902     ]
903
904     def inputDisarmHold = [
905         name:       "holdDisarm",
906         type:       "bool",
907         title:      "Hold to activate",
908         defaultValue: false,
909         required:   true
910     ]
911
912     def inputPanicButton = [
913         name:       "buttonPanic",
914         type:       "number",
915         title:      "Which button?",
916         required:   false
917     ]
918
919     def inputPanicHold = [
920         name:       "holdPanic",
921         type:       "bool",
922         title:      "Hold to activate",
923         defaultValue: false,
924         required:   true
925     ]
926
927     def pageProperties = [
928         name:       "pageRemoteOptions",
929         //title:      "Remote Control Options",
930         nextPage:   "pageSetup",
931         uninstall:  false
932     ]
933
934     return dynamicPage(pageProperties) {
935         section("Remote Control Options") {
936             paragraph helpRemote
937             input inputRemotes
938         }
939
940         section("Arm Away Button") {
941             input inputArmAwayButton
942             input inputArmAwayHold
943         }
944
945         section("Arm Stay Button") {
946             input inputArmStayButton
947             input inputArmStayHold
948         }
949
950         section("Disarm Button") {
951             input inputDisarmButton
952             input inputDisarmHold
953         }
954
955         section("Panic Button") {
956             input inputPanicButton
957             input inputPanicHold
958         }
959     }
960 }
961
962 // Show "REST API Options" page
963 def pageRestApiOptions() {
964     LOG("pageRestApiOptions()")
965
966     def textHelp =
967         "Smart Alarm can be controlled remotely by any Web client using " +
968         "REST API. Please refer to Smart Alarm documentation for more " +
969         "information."
970
971     def textPincode =
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."
976
977     def inputRestApi = [
978         name:           "restApiEnabled",
979         type:           "bool",
980         title:          "Enable REST API",
981         defaultValue:   false
982     ]
983
984     def inputPincode = [
985         name:           "pincode",
986         type:           "number",
987         title:          "PIN Code",
988         required:       false
989     ]
990
991     def inputArmWithPin = [
992         name:           "armWithPin",
993         type:           "bool",
994         title:          "Require PIN code to arm",
995         defaultValue:   true
996     ]
997
998     def pageProperties = [
999         name:       "pageRestApiOptions",
1000         //title:      "REST API Options",
1001         nextPage:   "pageSetup",
1002         uninstall:  false
1003     ]
1004
1005     return dynamicPage(pageProperties) {
1006         section("REST API Options") {
1007             paragraph textHelp
1008             input inputRestApi
1009         }
1010
1011         section("PIN Code") {
1012             paragraph textPincode
1013             input inputPincode
1014             input inputArmWithPin
1015         }
1016
1017         if (isRestApiEnabled()) {
1018             section("REST API Info") {
1019                 paragraph "App ID:\n${app.id}"
1020                 paragraph "Access Token:\n${state.accessToken}"
1021             }
1022         }
1023     }
1024 }
1025
1026 def installed() {
1027     LOG("installed()")
1028
1029     initialize()
1030     state.installed = true
1031 }
1032
1033 def updated() {
1034     LOG("updated()")
1035
1036     unschedule()
1037     unsubscribe()
1038     initialize()
1039 }
1040
1041 private def setupInit() {
1042     LOG("setupInit()")
1043
1044     if (state.installed == null) {
1045         state.installed = false
1046         state.armed = false
1047         state.zones = []
1048         state.alarms = []
1049         state.history = []
1050     } else {
1051         def version = state.version as String
1052         if (version == null || version.startsWith('1')) {
1053             return false
1054         }
1055     }
1056
1057     state.version = getVersion()
1058     return true
1059 }
1060
1061 private def initialize() {
1062     log.info "Smart Alarm. Version ${getVersion()}. ${textCopyright()}"
1063     LOG("settings: ${settings}")
1064
1065     clearAlarm()
1066     state.delay = settings.delay?.toInteger() ?: 30
1067     state.offSwitches = []
1068     state.history = []
1069
1070     if (settings.awayModes?.contains(location.mode)) {
1071         state.armed = true
1072         state.stay = false
1073     } else if (settings.stayModes?.contains(location.mode)) {
1074         state.armed = true
1075         state.stay = true
1076     } else {
1077         state.armed = false
1078         state.stay = false
1079     }
1080
1081     initZones()
1082     initButtons()
1083     initRestApi()
1084     subscribe(location, onLocation)
1085
1086     STATE()
1087 }
1088
1089 private def clearAlarm() {
1090     LOG("clearAlarm()")
1091
1092     state.alarms = []
1093     settings.alarms*.off()
1094
1095     // Turn off only those switches that we've turned on
1096     def switchesOff = state.offSwitches
1097     if (switchesOff) {
1098         LOG("switchesOff: ${switchesOff}")
1099         settings.switches.each() {
1100             if (switchesOff.contains(it.id)) {
1101                 it.off()
1102             }
1103         }
1104         state.offSwitches = []
1105     }
1106 }
1107
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"
1114
1115 private def initZones() {
1116     LOG("initZones()")
1117
1118     state.zones = []
1119
1120     state.zones << [
1121         deviceId:   null,
1122         sensorType: "panic",
1123         zoneType:   "alert",
1124         delay:      false
1125     ]
1126
1127     if (settings.z_contact) {
1128         settings.z_contact.each() {
1129             state.zones << [
1130                 deviceId:   it.id,
1131                 sensorType: "contact",
1132                 zoneType:   settings["type_${it.id}"] ?: "exterior",
1133                 delay:      settings["delay_${it.id}"]
1134             ]
1135         }
1136         subscribe(settings.z_contact, "contact.open", onContact)
1137     }
1138
1139     if (settings.z_motion) {
1140         settings.z_motion.each() {
1141             state.zones << [
1142                 deviceId:   it.id,
1143                 sensorType: "motion",
1144                 zoneType:   settings["type_${it.id}"] ?: "interior",
1145                 delay:      settings["delay_${it.id}"]
1146             ]
1147         }
1148         subscribe(settings.z_motion, "motion.active", onMotion)
1149     }
1150
1151     if (settings.z_movement) {
1152         settings.z_movement.each() {
1153             state.zones << [
1154                 deviceId:   it.id,
1155                 sensorType: "acceleration",
1156                 zoneType:   settings["type_${it.id}"] ?: "interior",
1157                 delay:      settings["delay_${it.id}"]
1158             ]
1159         }
1160         subscribe(settings.z_movement, "acceleration.active", onMovement)
1161     }
1162
1163     if (settings.z_smoke) {
1164         settings.z_smoke.each() {
1165             state.zones << [
1166                 deviceId:   it.id,
1167                 sensorType: "smoke",
1168                 zoneType:   settings["type_${it.id}"] ?: "alert",
1169                 delay:      settings["delay_${it.id}"]
1170             ]
1171         }
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)
1176     }
1177
1178     if (settings.z_water) {
1179         settings.z_water.each() {
1180             state.zones << [
1181                 deviceId:   it.id,
1182                 sensorType: "water",
1183                 zoneType:   settings["type_${it.id}"] ?: "alert",
1184                 delay:      settings["delay_${it.id}"]
1185             ]
1186         }
1187         subscribe(settings.z_water, "water.wet", onWater)
1188     }
1189
1190     state.zones.each() {
1191         def zoneType = it.zoneType
1192
1193         if (zoneType == "alert") {
1194             it.armed = true
1195         } else if (zoneType == "exterior") {
1196             it.armed = state.armed
1197         } else if (zoneType == "interior") {
1198             it.armed = state.armed && !state.stay
1199         } else {
1200             it.armed = false
1201         }
1202     }
1203 }
1204
1205 private def initButtons() {
1206     LOG("initButtons()")
1207
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"]
1214         }
1215
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"]
1220         }
1221
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"]
1226         }
1227
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"]
1232         }
1233
1234         if (state.buttonActions) {
1235             subscribe(settings.remotes, "button", onButtonEvent)
1236         }
1237     }
1238 }
1239
1240 private def initRestApi() {
1241     if (settings.restApiEnabled) {
1242         if (!state.accessToken) {
1243             def token = createAccessToken()
1244             LOG("Created new access token: ${token})")
1245         }
1246         state.url = "https://graph.api.smartthings.com/api/smartapps/installations/${app.id}/"
1247         log.info "REST API enabled"
1248     } else {
1249         state.url = ""
1250         log.info "REST API disabled"
1251     }
1252 }
1253
1254 private def isRestApiEnabled() {
1255     return settings.restApiEnabled && state.accessToken
1256 }
1257
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") }
1263
1264 private def onZoneEvent(evt, sensorType) {
1265     LOG("onZoneEvent(${evt.displayName}, ${sensorType})")
1266
1267     def zone = getZoneForDevice(evt.deviceId, sensorType)
1268     if (!zone) {
1269         log.warn "Cannot find zone for device ${evt.deviceId}"
1270         return
1271     }
1272
1273     if (zone.armed) {
1274         state.alarms << evt.displayName
1275         if (zone.zoneType == "alert" || !zone.delay || (state.stay && settings.stayDelayOff)) {
1276             activateAlarm()
1277         } else {
1278             myRunIn(state.delay, activateAlarm)
1279         }
1280     }
1281 }
1282
1283 def onLocation(evt) {
1284     LOG("onLocation(${evt.value})")
1285
1286     String mode = evt.value
1287     if (settings.awayModes?.contains(mode)) {
1288         armAway()
1289     } else if (settings.stayModes?.contains(mode)) {
1290         armStay()
1291     } else if (settings.disarmModes?.contains(mode)) {
1292         disarm()
1293     }
1294 }
1295
1296 def onButtonEvent(evt) {
1297     LOG("onButtonEvent(${evt.displayName})")
1298
1299     if (!state.buttonActions || !evt.data) {
1300         return
1301     }
1302
1303     def slurper = new JsonSlurper()
1304     def data = slurper.parseText(evt.data)
1305     def button = data.buttonNumber?.toInteger()
1306     if (button) {
1307         LOG("Button '${button}' was ${evt.value}.")
1308         def item = state.buttonActions.find {
1309             it.button == button && it.event == evt.value
1310         }
1311
1312         if (item) {
1313             LOG("Executing '${item.action}' button action")
1314             "${item.action}"()
1315         }
1316     }
1317 }
1318
1319 def armAway() {
1320     LOG("armAway()")
1321
1322     if (!atomicState.armed || atomicState.stay) {
1323         armPanel(false)
1324     }
1325 }
1326
1327 def armStay() {
1328     LOG("armStay()")
1329
1330     if (!atomicState.armed || !atomicState.stay) {
1331         armPanel(true)
1332     }
1333 }
1334
1335 def disarm() {
1336     LOG("disarm()")
1337
1338     if (atomicState.armed) {
1339         state.armed = false
1340         state.zones.each() {
1341             if (it.zoneType != "alert") {
1342                 it.armed = false
1343             }
1344         }
1345
1346         reset()
1347     }
1348 }
1349
1350 def panic() {
1351     LOG("panic()")
1352
1353     state.alarms << "Panic"
1354     activateAlarm()
1355 }
1356
1357 def reset() {
1358     LOG("reset()")
1359
1360     unschedule()
1361     clearAlarm()
1362
1363     // Send notification
1364     def msg = "${location.name} is "
1365     if (state.armed) {
1366         msg += "ARMED "
1367         msg += state.stay ? "STAY" : "AWAY"
1368     } else {
1369         msg += "DISARMED."
1370     }
1371
1372     notify(msg)
1373     notifyVoice()
1374 }
1375
1376 def exitDelayExpired() {
1377     LOG("exitDelayExpired()")
1378
1379     def armed = atomicState.armed
1380     def stay = atomicState.stay
1381     if (!armed) {
1382         log.warn "exitDelayExpired: unexpected state!"
1383         STATE()
1384         return
1385     }
1386
1387     state.zones.each() {
1388         def zoneType = it.zoneType
1389         if (zoneType == "exterior" || (zoneType == "interior" && !stay)) {
1390             it.armed = true
1391         }
1392     }
1393
1394     def msg = "${location.name}: all "
1395     if (stay) {
1396         msg += "exterior "
1397     }
1398     msg += "zones are armed."
1399
1400     notify(msg)
1401 }
1402
1403 private def armPanel(stay) {
1404     LOG("armPanel(${stay})")
1405
1406     unschedule()
1407     clearAlarm()
1408
1409     state.armed = true
1410     state.stay = stay
1411
1412     def armDelay = false
1413     state.zones.each() {
1414         def zoneType = it.zoneType
1415         if (zoneType == "exterior") {
1416             if (it.delay) {
1417                 it.armed = false
1418                 armDelay = true
1419             } else {
1420                 it.armed = true
1421             }
1422         } else if (zoneType == "interior") {
1423             if (stay) {
1424                 it.armed = false
1425             } else if (it.delay) {
1426                 it.armed = false
1427                 armDelay = true
1428             } else {
1429                 it.armed = true
1430             }
1431         }
1432     }
1433
1434     def delay = armDelay && !(stay && settings.stayDelayOff) ? atomicState.delay : 0
1435     if (delay) {
1436         myRunIn(delay, exitDelayExpired)
1437     }
1438
1439     def mode = stay ? "STAY" : "AWAY"
1440     def msg = "${location.name} "
1441     if (delay) {
1442         msg += "will arm ${mode} in ${state.delay} seconds."
1443     } else {
1444         msg += "is ARMED ${mode}."
1445     }
1446
1447     notify(msg)
1448     notifyVoice()
1449 }
1450
1451 // .../armaway REST API endpoint
1452 def apiArmAway() {
1453     LOG("apiArmAway()")
1454
1455     if (!isRestApiEnabled()) {
1456         log.error "REST API disabled"
1457         return httpError(403, "Access denied")
1458     }
1459
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")
1463     }
1464
1465     armAway()
1466     return apiStatus()
1467 }
1468
1469 // .../armstay REST API endpoint
1470 def apiArmStay() {
1471     LOG("apiArmStay()")
1472
1473     if (!isRestApiEnabled()) {
1474         log.error "REST API disabled"
1475         return httpError(403, "Access denied")
1476     }
1477
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")
1481     }
1482
1483     armStay()
1484     return apiStatus()
1485 }
1486
1487 // .../disarm REST API endpoint
1488 def apiDisarm() {
1489     LOG("apiDisarm()")
1490
1491     if (!isRestApiEnabled()) {
1492         log.error "REST API disabled"
1493         return httpError(403, "Access denied")
1494     }
1495
1496     if (settings.pincode && (params.pincode != settings.pincode.toString())) {
1497         log.error "Invalid PIN code '${params.pincode}'"
1498         return httpError(403, "Access denied")
1499     }
1500
1501     disarm()
1502     return apiStatus()
1503 }
1504
1505 // .../panic REST API endpoint
1506 def apiPanic() {
1507     LOG("apiPanic()")
1508
1509     if (!isRestApiEnabled()) {
1510         log.error "REST API disabled"
1511         return httpError(403, "Access denied")
1512     }
1513
1514     panic()
1515     return apiStatus()
1516 }
1517
1518 // .../status REST API endpoint
1519 def apiStatus() {
1520     LOG("apiStatus()")
1521
1522     if (!isRestApiEnabled()) {
1523         log.error "REST API disabled"
1524         return httpError(403, "Access denied")
1525     }
1526
1527     def status = [
1528         status: state.armed ? (state.stay ? "armed stay" : "armed away") : "disarmed",
1529         alarms: state.alarms
1530     ]
1531
1532     return status
1533 }
1534
1535 def activateAlarm() {
1536     LOG("activateAlarm()")
1537
1538     if (state.alarms.size() == 0) {
1539         log.warn "activateAlarm: false alarm"
1540         return
1541     }
1542
1543     switch (settings.sirenMode) {
1544     case "Siren":
1545         settings.alarms*.siren()
1546         break
1547
1548     case "Strobe":
1549         settings.alarms*.strobe()
1550         break
1551         
1552     case "Both":
1553         settings.alarms*.both()
1554         break
1555     }
1556
1557     // Only turn on those switches that are currently off
1558     def switchesOn = settings.switches?.findAll { it?.currentSwitch == "off" }
1559     LOG("switchesOn: ${switchesOn}")
1560     if (switchesOn) {
1561         switchesOn*.on()
1562         state.offSwitches = switchesOn.collect { it.id }
1563     }
1564
1565     settings.cameras*.take()
1566
1567     if (settings.helloHomeAction) {
1568         log.info "Executing HelloHome action '${settings.helloHomeAction}'"
1569         location.helloHome.execute(settings.helloHomeAction)
1570     }
1571
1572     def msg = "Alarm at ${location.name}!"
1573     state.alarms.each() {
1574         msg += "\n${it}"
1575     }
1576
1577     notify(msg)
1578     notifyVoice()
1579
1580     myRunIn(180, reset)
1581 }
1582
1583 private def notify(msg) {
1584     LOG("notify(${msg})")
1585
1586     log.info msg
1587
1588     if (state.alarms.size()) {
1589         // Alarm notification
1590         if (settings.pushMessage) {
1591             mySendPush(msg)
1592         } else {
1593             sendNotificationEvent(msg)
1594         }
1595
1596         if (settings.smsAlarmPhone1 && settings.phone1) {
1597             sendSms(phone1, msg)
1598         }
1599
1600         if (settings.smsAlarmPhone2 && settings.phone2) {
1601             sendSms(phone2, msg)
1602         }
1603
1604         if (settings.smsAlarmPhone3 && settings.phone3) {
1605             sendSms(phone3, msg)
1606         }
1607
1608         if (settings.smsAlarmPhone4 && settings.phone4) {
1609             sendSms(phone4, msg)
1610         }
1611
1612         if (settings.pushbulletAlarm && settings.pushbullet) {
1613             settings.pushbullet*.push(location.name, msg)
1614         }   
1615     } else {
1616         // Status change notification
1617         if (settings.pushStatusMessage) {
1618             mySendPush(msg)
1619         } else {
1620             sendNotificationEvent(msg)
1621         }
1622
1623         if (settings.smsStatusPhone1 && settings.phone1) {
1624             sendSms(phone1, msg)
1625         }
1626
1627         if (settings.smsStatusPhone2 && settings.phone2) {
1628             sendSms(phone2, msg)
1629         }
1630
1631         if (settings.smsStatusPhone3 && settings.phone3) {
1632             sendSms(phone3, msg)
1633         }
1634
1635         if (settings.smsStatusPhone4 && settings.phone4) {
1636             sendSms(phone4, msg)
1637         }
1638
1639         if (settings.pushbulletStatus && settings.pushbullet) {
1640             settings.pushbullet*.push(location.name, msg)
1641         }
1642     }
1643 }
1644
1645 private def notifyVoice() {
1646     LOG("notifyVoice()")
1647
1648     if (!settings.audioPlayer) {
1649         return
1650     }
1651
1652     def phrase = null
1653     if (state.alarms.size()) {
1654         // Alarm notification
1655         if (settings.speechOnAlarm) {
1656             phrase = settings.speechText ?: getStatusPhrase()
1657         }
1658     } else {
1659         // Status change notification
1660         if (settings.speechOnStatus) {
1661             if (state.armed) {
1662                 if (state.stay) {
1663                     phrase = settings.speechTextArmedStay ?: getStatusPhrase()
1664                 } else {
1665                     phrase = settings.speechTextArmedAway ?: getStatusPhrase()
1666                 }
1667             } else {
1668                 phrase = settings.speechTextDisarmed ?: getStatusPhrase()
1669             }
1670         }
1671     }
1672
1673     if (phrase) {
1674         settings.audioPlayer*.playText(phrase)
1675     }
1676 }
1677
1678 private def history(String event, String description = "") {
1679     LOG("history(${event}, ${description})")
1680
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]
1686     }
1687
1688     LOG("history: ${history}")
1689     state.history = history
1690 }
1691
1692 private def getStatusPhrase() {
1693     LOG("getStatusPhrase()")
1694
1695     def phrase = ""
1696     if (state.alarms.size()) {
1697         phrase = "Alarm at ${location.name}!"
1698         state.alarms.each() {
1699             phrase += " ${it}."
1700         }
1701     } else {
1702         phrase = "${location.name} security is "
1703         if (state.armed) {
1704             def mode = state.stay ? "stay" : "away"
1705             phrase += "armed in ${mode} mode."
1706         } else {
1707             phrase += "disarmed."
1708         }
1709     }
1710
1711     return phrase
1712 }
1713
1714 private def getHelloHomeActions() {
1715     def actions = location.helloHome?.getPhrases().collect() { it.label }
1716     return actions.sort()
1717 }
1718
1719 private def getAlarmStatus() {
1720     def alarmStatus
1721
1722     if (atomicState.armed) {
1723         alarmStatus = "ARMED "
1724         alarmStatus += atomicState.stay ? "STAY" : "AWAY"
1725     } else {
1726         alarmStatus = "DISARMED"
1727     }
1728
1729     return alarmStatus
1730 }
1731
1732 private def getZoneStatus(device, sensorType) {
1733
1734     def zone = getZoneForDevice(device.id, sensorType)
1735     if (!zone) {
1736         return null
1737     }
1738
1739     def str = "${device.displayName}: ${zone.zoneType}, "
1740     str += zone.armed ? "armed, " : "disarmed, "
1741     str += device.currentValue(sensorType)
1742
1743     return str
1744 }
1745
1746 private def getZoneForDevice(id, sensorType) {
1747     return state.zones.find() { it.deviceId == id && it.sensorType == sensorType }
1748 }
1749
1750 private def isZoneReady(device, sensorType) {
1751     def ready
1752
1753     switch (sensorType) {
1754     case "contact":
1755         ready = "closed".equals(device.currentValue("contact"))
1756         break
1757
1758     case "motion":
1759         ready = "inactive".equals(device.currentValue("motion"))
1760         break
1761
1762     case "acceleration":
1763         ready = "inactive".equals(device.currentValue("acceleration"))
1764         break
1765
1766     case "smoke":
1767         ready = "clear".equals(device.currentValue("smoke"))
1768         break
1769
1770     case "water":
1771         ready = "dry".equals(device.currentValue("water"))
1772         break
1773
1774     default:
1775         ready = false
1776     }
1777
1778     return ready
1779 }
1780
1781 private def getDeviceById(id, sensorType) {
1782     switch (sensorType) {
1783     case "contact":
1784         return settings.z_contact?.find() { it.id == id }
1785
1786     case "motion":
1787         return settings.z_motion?.find() { it.id == id }
1788
1789     case "acceleration":
1790         return settings.z_movement?.find() { it.id == id }
1791
1792     case "smoke":
1793         return settings.z_smoke?.find() { it.id == id }
1794
1795     case "water":
1796         return settings.z_water?.find() { it.id == id }
1797     }
1798
1799     return null
1800 }
1801
1802 private def getNumZones() {
1803     def numZones = 0
1804
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
1810
1811     return numZones
1812 }
1813
1814 private def myRunIn(delay_s, func) {
1815     if (delay_s > 0) {
1816         def date = new Date(now() + (delay_s * 1000))
1817         runOnce(date, func)
1818         LOG("scheduled '${func}' to run at ${date}")
1819     }
1820 }
1821
1822 private def mySendPush(msg) {
1823     // sendPush can throw an exception
1824     try {
1825         sendPush(msg)
1826     } catch (e) {
1827         log.error e
1828     }
1829 }
1830
1831 private def getVersion() {
1832     return "2.4.3"
1833 }
1834
1835 private def textCopyright() {
1836     def text = "Copyright © 2014 Statusbits.com"
1837 }
1838
1839 private def textLicense() {
1840     def text =
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/>."
1851 }
1852
1853 private def LOG(message) {
1854     //log.trace message
1855 }
1856
1857 private def STATE() {
1858     //log.trace "state: ${state}"
1859 }