├── LICENSE
├── README.md
├── devicetypes
└── alarmdecoder
│ ├── alarmdecoder-action-button-indicator.src
│ └── alarmdecoder-action-button-indicator.groovy
│ ├── alarmdecoder-network-appliance.src
│ └── alarmdecoder-network-appliance.groovy
│ ├── alarmdecoder-status-indicator.src
│ └── alarmdecoder-status-indicator.groovy
│ ├── alarmdecoder-virtual-carbon-monoxide-detector.src
│ └── alarmdecoder-virtual-carbon-monoxide-detector.groovy
│ ├── alarmdecoder-virtual-contact-sensor.src
│ └── alarmdecoder-virtual-contact-sensor.groovy
│ ├── alarmdecoder-virtual-motion-detector.src
│ └── alarmdecoder-virtual-motion-detector.groovy
│ ├── alarmdecoder-virtual-shock-sensor.src
│ └── alarmdecoder-virtual-shock-sensor.groovy
│ └── alarmdecoder-virtual-smoke-alarm.src
│ └── alarmdecoder-virtual-smoke-alarm.groovy
└── smartapps
└── alarmdecoder
└── alarmdecoder-service.src
└── alarmdecoder-service.groovy
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TOC
2 |
3 | - [Introduction](#introduction)
4 | - [Requirements](#requirements)
5 | - [Features](#features)
6 | - [Setup](#setup)
7 | -- [Install device handlers (via github integration)](#install-device-handlers--via-github-integration-)
8 | -- [Install SmartApp (via github integration)](#install-smartapp--via-github-integration-)
9 | -- [Obtain an API key from the AlarmDecoder webapp](#obtain-an-api-key-from-the-alarmdecoder-webapp)
10 | -- [Enabling SmartThings/Hubitat UPNP/SSDP Integration in the Webapp](#enabling-smartthings-hubitat-upnp-ssdp-integration-in-the-webapp)
11 | -- [Configure AlarmDecoder device](#configure-alarmdecoder-device)
12 | -- [Configure Contact ID switches](#configure-contact-id-switches)
13 | -- [Configure RFX switches for Ademco 58XX sensors](#configure-rfx-switches-for-ademco-58xx-sensors)
14 | - [Additional Info](#additional-info)
15 | -- [Provided virtual devices](#provided-virtual-devices)
16 | -- [Alarm Decoder Device Handlers](#alarm-decoder-device-handlers)
17 | -- [Known Issues](#known-issues)
18 |
19 | # Introduction
20 |
21 | This project provides support for the AlarmDecoder webapp UPNP/SSDP integration with SmartThings or Hubitat home automation platforms.
22 |
23 | # Requirements
24 |
25 | The AlarmDecoder webapp and hub use UDP broadcast packets for discovery. The hub and webapp must be located on the same layer 2 network.
26 |
27 | - AlarmDecoder webapp 0.8.3+
28 | - SmartThings or Hubitat hub
29 |
30 | # Features
31 |
32 | - Arm, disarm, toggle chime, or panic your alarm system from within SmartThings/Hubitat.
33 | - Provides virtual sensors that can be tied to zones on your panel to allow automation based on zones faulting and restoring.
34 | - Provides virtual momentary switches with indicators for arming away, stay and toggling chime mode with each switch indicating the current state. Alexa[#warn](#warn) and other systems are able to activate these switches to allow a wide array of alarm panel control possibilities.
35 | - Provide virtual contact sensors for "Ready" and "Alarm Bell" indications on the alarm panel.
36 | - Provides a virtual "Smoke Alarm" that can be integrated with SHM or other systems to control state during a fire event such as turning all lights on.
37 | - Provides the ability to create virtual switches that can be tied to specific Contact ID report codes from the alarm panel. As an example a zone setup as a Carbon Monoxide alarm can be directly tied to a virtual switch that will OPEN in the event it triggers using Contact ID 162.
38 | - Provides the ability to create virtual switches that can be tied to specific 5800 RF devices. Typical use case is using a 5800micra sensor to monitor open/close of secure areas but not require panel zone programming to use. Just record its serial number and tie it to a virtual device using the serial number only.
39 | - Smart Home Monitor / Home Security Module integration.
40 | One-way - Arm or disarm your panel when the Smart Home Monitor status is changed.
41 | Two-way - Change Smart Home Monitor's status when your panel is armed or disarmed.
42 | - Change virtual device handlers in the graph pages to change device capabilities and the system will adjust event types to match the device. Change a Zone Sensor to a Virtual Smoke Alarm and it will report 'clear' or 'detected'. This allows changing of device types to match what is needed for the task.
43 |
44 | #warn: Alexa and other systems may be confused by the device name to send ON or OFF action too. This can be done using fuzzy AI logic than can cause the system to send the event to a Panic or Alarm action button triggering an alarm. Thoughtful naming of devices as well as restricting access is advised before allowing these external systems to access the virtual buttons.
45 |
46 | # Setup
47 |
48 | Navigate to in your browser and login to your account.
49 |
50 | ## Install device handlers (via github integration)
51 |
52 | - Be sure a Hub is associated to the Location you are installing the service into.
53 |
54 | 1. Click on **My Device Handlers**
55 | 2. Click **Settings** (top of page)
56 | 3. Click **Add New Repository** (bottom of dialog)
57 | 4. Enter `nutechsoftware` as the **owner**
58 | 5. Enter `alarmdecoder-smartthings` as the **name**
59 | 6. Enter `master` as the **branch**
60 | 7. Click **Save**
61 | 8. Click **Update From Repo** (top of page)
62 | 9. Check the boxes
63 | - [x] `AlarmDecoder network appliance`
64 | - [x] `AlarmDecoder virtual contact sensor`
65 | - [x] `AlarmDecoder virtual smoke alarm`
66 | - [x] `AlarmDecoder action button indicator`
67 | - [x] `AlarmDecoder status indicator`
68 | - [x] `AlarmDecoder virtual shock sensor`
69 | - [x] `AlarmDecoder virtual motion detector`
70 | - [x] `AlarmDecoder virtual carbon monoxide detector`
71 | 10. Check **Publish** (bottom of dialog)
72 | 11. Click **Execute Update**
73 |
74 | ## Install SmartApp (via github integration)
75 |
76 | 1. Click on **My SmartApps**
77 | 2. Click **Update From Repo** (top of page)
78 | 3. Check box for `alarmdecoder service`
79 | 4. Check **Publish** (bottom of dialog)
80 | 5. Click **Execute Update**
81 | 6. Adjust **@Field** settings as needed at the top of the **AlarmDecoder service** code and as well as any other noted changes needed for Hubitat or SmartThings in the header and **Publish** if changes are made.
82 | 7. Select the `alarmdecoder: AlarmDecoder service` smart app and then select your location on the right and press **Set Location**. (Click the **Simulator** if you don't see these options)
83 | 8. Click the **Discover** button. You may have to hit refresh to get your device to show up. If it doesn't show up make sure you're running an up-to-date version of the webapp and that it is on the same netowrk as your SmartThings HUB.
84 | 9. Click **Select Devices** and select your AlarmDecoder.
85 | 10. Click **Install**
86 |
87 | - notes
88 | a. This will generate new devices under **My Devices**
89 | b. If you **Uninstall** from **AlarmDecoder service** screen it will attempt to automatically remove all sub devices if they are not in use by SHM or other rules.
90 | c. You can remove blocking child items from the **My Devices** -> **Show Device** screen by selecting the **In Use By** item and deleting it.
91 |
92 | ## Obtain an API key from the AlarmDecoder webapp
93 |
94 | 1. Navigate to the API section of webapp on your local network: ()
95 | 2. Click **Manage API keys**
96 | 3. Click **Generate** for the desired webapp user (eg. `admin`)
97 |
98 | ## Enabling SmartThings/Hubitat UPNP/SSDP Integration in the Webapp
99 |
100 | 1. Log into your AlarmDecoder webapp.
101 | 2. Click Settings
102 | 3. Click Notifications
103 | 4. Click the New Notification button
104 | 5. Set the Notification Type to 'UPNP Push'
105 | 6. Enter a description ex 'UPNP PUSH'
106 | 7. Press Next
107 | 8. Press Save
108 |
109 | - notes
110 | a. If the AlarmDecoder Web App restarts it will loose subscriptions. It may take 5 minutes to restore PUSH notification.
111 | b. Updating the **AlarmDecoder UI** device settings on the phone app or web-based IDE will force a new subscription.
112 |
113 | ## Configure AlarmDecoder device
114 |
115 | Using the SmartThings app **on your phone**
116 | 1. Open up the SmartThings app **on your phone**
117 | 2. Tap **My Home** and select the **Things** tab
118 | 3. Select the **AlarmDecoder UI** device
119 | 4. Tap the gear icon to edit the device
120 | 5. Enter the API key you generated from the AlarmDecoder webapp
121 | 6. Enter the alarm code you'd like to use to arm/disarm your panel.
122 | 7. Select your panel type.
123 | 8. Zone sensors may be configured to open and close themselves when a zone is faulted. For example, specifying zone 7 for Zonetracker Sensor #1 would trip that sensor whenever zone 7 is faulted.
124 | 9. Use **graph.api.smartthings.com** and modify the device type in the device editor. If the application needs the device to be a Smoke Alarm then change its device type to **AlarmDecoder virtual smoke alarm** in the device editor.
125 | 10. Using the devices preferences(gear) update the zone numbers and invert option for the **Zone Sensors**.
126 |
127 | Using **graph.api.smartthings.com**
128 | 1. Login to your SmartThings graph web-based IDE.
129 | 2. Select **My Devices**
130 | 3. Select the **AlarmDecoder(AD2)** device for your HUBs location.
131 | 4. Click Preferences(**edit**) link.
132 | 5. Enter the Rest API key you generated from the AlarmDecoder webapp
133 | 6. Enter the alarm code you'd like to use to arm/disarm your panel.
134 | 7. In the Panel Type - Type of panel enter **ADEMCO** or **DSC** depending on the panel type.
135 | 8. Change the **Device Type** of the new **Zone Sensors** as desired by editing the device. If the application needs the device to be a Smoke Alarm then change its device type to **AlarmDecoder virtual smoke alarm** in the device editor.
136 | 9. Using the preferences page update the zone numbers and invert option for the **Zone Sensors**.
137 |
138 | ## Configure Contact ID switches
139 |
140 | **CID** or **Contact ID** is a prevalent and respected format for communications between alarms and the systems at alarm monitoring agencies that they report to.
141 |
142 | If the Ademco panel has an existing Internet or cellular communicator then the AD2 is able to capture these messages and PUSH them to the home automation system as virtual switches. For DSC Power Series panels this feature is a Zero configuration feature(it just works). With Ademco panels a communicator is required or you can enable LRR messaging in your panel programming and enable LRR Emulation on the AlarmDecoder. In both panels it may be necessary to Enable / Disable reporting for some types of events.
143 |
144 | 1. Open up the SmartThings app **on your phone**
145 | 2. Tap **My Home** and select the **Things** tab
146 | 3. Select the **AlarmDecoder(AD2)** device
147 | 4. Select the **SmartApps** tab
148 | 5. Select the **AlarmDecoder service**
149 | 6. Select **Contact ID device management**
150 | 7. Select **Add new CID virtual switch**
151 | 8. Select the CID number pattern
152 | -- 000 - Other / Custom
153 | -- 100-102 - ALL Medical alarms
154 | -- 110-118 - ALL Fire alarms
155 | -- 120-126 - ALL Panic alarms
156 | -- 130-139 - ALL Burglar alarms
157 | -- 140-149 - ALL General alarms
158 | -- 150-169 - ALL 24 HOUR AUX alarms
159 | -- 154 - Water Leakage
160 | -- 158 - High Temp
161 | -- 162 - Carbon Monoxide Detected
162 | -- 301 - AC Loss
163 | -- 3?? - All System Troubles
164 | -- 401 - Arm AWAY OPEN/CLOSE
165 | -- 441 - Arm STAY OPEN/CLOSE
166 | -- 4[0,4]1 - Arm Stay or Away OPEN/CLOSE
167 | 9. Enter the User or ZONE to match or ??? to match all.
168 | 10. Enter the partition to match 0(system), 1, 2 or ? to match all.
169 | 11. select **Add new CID virtual switch**
170 | 12. The switch will be created and you can see it under **My Devices**
171 | 13. Change the **Device Type** of the new **CID virtual switch** as desired by editing the device. If the application needs the device to be a Smoke Alarm then change its device type to **AlarmDecoder virtual smoke alarm** in the device editor.
172 |
173 | ## Configure RFX switches for Ademco 58XX sensors
174 |
175 | All 5800 sensors within range of the 5800 receiver are able to be monitored for events. All that is needed is the serial number. It is not necessary to program the 5800 device into the panel for this to work.
176 |
177 | 1. Open up the SmartThings app **on your phone**
178 | 2. Tap **My Home** and select the **Things** tab
179 | 3. Select the **AlarmDecoder(AD2)** device
180 | 4. Select the **SmartApps** tab
181 | 5. Select the **AlarmDecoder service**
182 | 6. Select **RFX device management**
183 | 7. Select **Add new RFX virtual switch**
184 | 8. Enter the 7 digit serial number including leading 0's ex 0123020
185 | 9. Enter **?**, **0** or **1** for each loop or attribute to watch for events. ? will ignore the attribute and a value will match for it.
186 | 10. select **Add new RFX switch**
187 | 11. The switch will be created and you can see it under **My Devices**
188 | 12. Change the **Device Type** of the new **RFX virtual switch** as desired by editing the device. If the application needs the device to be a Smoke Alarm then change its device type to **AlarmDecoder virtual smoke alarm** in the device editor.
189 |
190 | # Additional Info
191 |
192 | ## Provided virtual devices
193 |
194 | - AlarmDecoder UI
195 | Description: Main service device provides a simple user interface to manage the alarm.
196 | - Security[#1](#vdevicenames) Alarm Bell
197 | Network Mask: \*:alarmBell
198 | Description: An indicator to show the panel bell state and button to clear.
199 | Default Handler: **AlarmDecoder action button indicator**[#5](#flexible_handlers)
200 | -- capabilities [Momentary, Switch]
201 | -- actions [push: manually set state to off and clear]
202 | -- states [on(Alarming), off]
203 | -- preferences \[ invert:[true, false], zone: N/A]
204 | - Security[#1](#vdevicenames) Alarm Bell Status
205 | Network Mask: \*:alarmBellStatus
206 | Description: An indicator to show the panel bell state.
207 | Default Handler: **AlarmDecoder status indicator**[#5](#flexible_handlers)
208 | -- capabilities [Contact sensor]
209 | -- states [open(Alarming), close]
210 | -- preferences \[ invert:[true, false], zone: N/A]
211 | - Security[#1](#vdevicenames) Chime
212 | Network Mask: \*:chimeMode
213 | Description: indicator to show the Chime state and button to toggle.
214 | Default Handler: **AlarmDecoder action button indicator**[#5](#flexible_handlers)
215 | -- capabilities [Momentary, Switch]
216 | -- actions [push: toggle chime mode]
217 | -- states [on(Enabled), off]
218 | -- preferences \[ invert:[true, false], zone: N/A]
219 | - Security[#1](#vdevicenames) Chime Status
220 | Network Mask: \*:chimeModeStatus
221 | Description: An indicator to show the chime state.
222 | Default Handler: **AlarmDecoder status indicator**[#5](#flexible_handlers)
223 | -- capabilities [Contact sensor]
224 | -- states [open(Enabled), close]
225 | -- preferences \[ invert:[true, false], zone: N/A]
226 | - Security[#1](#vdevicenames) Ready Status
227 | Network Mask: \*:readyStatus
228 | Description: An indicator to show the panel ready to arm state.
229 | Default Handler: **AlarmDecoder status indicator**[#5](#flexible_handlers)
230 | -- capabilities [Contact sensor]
231 | -- states [open(READY), close]
232 | -- preferences \[ invert:[true, false], zone: N/A]
233 | - Security[#1](#vdevicenames) Bypass Status
234 | Network Mask: \*:bypassStatus
235 | Description: An indicator to show if the panel has a bypassed zone.
236 | Default Handler: **AlarmDecoder status indicator**[#5](#flexible_handlers)
237 | -- capabilities [Contact sensor]
238 | -- states [open(Zone(s) bypassed), close]
239 | -- preferences \[ invert:[true, false], zone: N/A]
240 | - Security[#1](#vdevicenames) Entry Delay Off state
241 | Network Mask: \*:entryDelayOffStatus
242 | Description: An indicator to show if the panel has exit delay off set.
243 | Default Handler: **AlarmDecoder status indicator**[#5](#flexible_handlers)
244 | -- capabilities [Contact sensor]
245 | -- states [open(Exit Delay OFF), close]
246 | -- preferences \[ invert:[true, false], zone: N/A]
247 | - Security[#1](#vdevicenames) Perimeter Only state
248 | Network Mask: \*:perimeterOnlyStatus
249 | Description: An indicator to show if the panel has perimeter only set.
250 | Default Handler: **AlarmDecoder status indicator**[#5](#flexible_handlers)
251 | -- capabilities [Contact sensor]
252 | -- states [open(Perimeter Only ON), close]
253 | -- preferences \[ invert:[true, false], zone: N/A]
254 | - Security[#1](#vdevicenames) Smoke Alarm
255 | Network Mask: \*:smokeAlarm
256 | Description: An indicator to show the panel fire state.
257 | Default Handler: **AlarmDecoder virtual smoke alarm**[#5](#flexible_handlers)
258 | -- capabilities [Smoke Detector]
259 | -- states [clear, detected]
260 | -- preferences \[ invert:[true, false], zone: N/A]
261 | - Security[#1](#vdevicenames) Disarm
262 | Network Mask: \*:disarm
263 | Description: indicator to show the arm state and button to disarm.
264 | Default Handler: **AlarmDecoder action button indicator**[#5](#flexible_handlers)
265 | -- capabilities [Momentary, Switch]
266 | -- actions [push: disarm panel]
267 | -- states [on(Armed), off]
268 | -- preferences \[ invert:[true, false], zone: N/A]
269 | - Security[#1](#vdevicenames) Stay
270 | Network Mask: \*:armStay
271 | Description: indicator to show the arm stay state and button to arm stay.
272 | Default Handler: **AlarmDecoder action button indicator**[#5](#flexible_handlers)
273 | -- capabilities [Momentary, Switch]
274 | -- actions [push: arm stay]
275 | -- states [on(Armed Stay), off]
276 | -- preferences \[ invert:[true, false], zone: N/A]
277 | - Security[#1](#vdevicenames) Stay Status
278 | Network Mask: \*:armStayStatus
279 | Description: An indicator to show if the panel is armed in stay mode.
280 | Default Handler: **AlarmDecoder status indicator**[#5](#flexible_handlers)
281 | -- capabilities [Contact sensor]
282 | -- states [open(Armed Stay), close]
283 | -- preferences \[ invert:[true, false], zone: N/A]
284 | - Security[#1](#vdevicenames) Away
285 | Network Mask: \*:armAway
286 | Description: indicator to show the arm away state and button to arm away.
287 | Default Handler: **AlarmDecoder action button indicator**[#5](#flexible_handlers)
288 | -- capabilities [Momentary, Switch]
289 | -- actions [push: arm away]
290 | -- states [on(Armed Away), off]
291 | -- preferences \[ invert:[true, false], zone: N/A]
292 | - Security[#1](#vdevicenames) Away Status
293 | Network Mask: \*:armAwayStatus
294 | Description: An indicator to show if the panel is armed in stay mode.
295 | Default Handler: **AlarmDecoder status indicator**[#5](#flexible_handlers)
296 | -- capabilities [Contact sensor]
297 | -- states [open(Armed Away), close]
298 | -- preferences \[ invert:[true, false], zone: N/A]
299 | - Security[#1](#vdevicenames) Exit
300 | Network Mask: \*:exit
301 | Description: indicator to show the exit state.
302 | Default Handler: **AlarmDecoder action button indicator**[#5](#flexible_handlers)
303 | -- capabilities [Momentary, Switch]
304 | -- actions [push: request exit]
305 | -- states [on(Exit now active), off]
306 | -- preferences \[ invert:[true, false], zone: N/A]
307 | - Security[#1](#vdevicenames) Exit Status
308 | Network Mask: \*:exitStatus
309 | Description: An indicator to show if the panel exit mode is active.
310 | Default Handler: **AlarmDecoder status indicator**[#5](#flexible_handlers)
311 | -- capabilities [Contact sensor]
312 | -- states [open(Exit now active), close]
313 | -- preferences \[ invert:[true, false], zone: N/A]
314 | - Security[#1](#vdevicenames) Panic Alarm
315 | Network Mask: \*:alarmPanic
316 | Description: Button to send Panic Alarm signal to the panel.
317 | Default Handler: **AlarmDecoder action button indicator**[#5](#flexible_handlers)
318 | -- capabilities [Momentary, Switch]
319 | -- actions [push: request panic]
320 | -- states N/A[#1](#notsupported)
321 | \-- preferences \[ invert:[true, false], zone: N/A]
322 | - Security[#1](#vdevicenames) AUX(Medical) Alarm
323 | Network Mask: \*:alarmAUX
324 | Description: Button to send AUX(Medical) Alarm signal to the panel.
325 | Default Handler: **AlarmDecoder action button indicator**[#5](#flexible_handlers)
326 | -- capabilities [Momentary, Switch]
327 | -- actions [push: request aux alarm]
328 | -- states N/A[#1](#notsupported)
329 | \-- preferences \[ invert:[true, false], zone: N/A]
330 | - Security[#1](#vdevicenames) Fire Alarm
331 | Network Mask: \*:alarmFire
332 | Description: An indicator to show the panel fire state and button to clear.
333 | Default Handler: **AlarmDecoder action button indicator**[#5](#flexible_handlers)
334 | -- capabilities [Momentary, Switch]
335 | -- actions [push: manually set state to off and clear]
336 | -- states [on(Alarming), off]
337 | -- preferences \[ invert:[true, false], zone: N/A]
338 | - Security[#1](#vdevicenames) Fire Alarm Status
339 | Network Mask: \*:alarmFireStatus
340 | Description: An indicator to show the panel fire alarm state.
341 | Default Handler: **AlarmDecoder status indicator**[#5](#flexible_handlers)
342 | -- capabilities [Contact sensor]
343 | -- states [open(Alarming), close]
344 | -- preferences \[ invert:[true, false], zone: N/A]
345 | - Security[#1](#vdevicenames) Zone Sensor #N[#4](#zonenumbers)
346 | Network Mask: \*:switch[#N]
347 | Description: An indicator to show the zone state.
348 | Default Handler: **AlarmDecoder virtual contact sensor**[#5](#flexible_handlers)
349 | -- capabilities [Contact sensor]
350 | -- states [open(Alarming), close]
351 | -- preferences \[ invert:[true, false], zone: Numeric zone associated with this device.]
352 | - CID-**_AAA_**-**_B_**-**_CCC_**[#2](#cidmask)
353 | Network Mask: \*:CID-AAAA-B-CCC
354 | Description: Indicates the state of the given Contact ID report state.
355 | Default Handler: **AlarmDecoder action button indicator**[#5](#flexible_handlers)
356 | -- capabilities [Contact sensor]
357 | -- actions [push: manually set state to off and clear]
358 | -- states [on(Active), off]
359 | -- preferences \[ invert:[true, false], zone: N/A]
360 | - RFX-AAAAAA-B-C-D-E-F-G[#3](#rfxmask)
361 | Network Mask: \*:RFX-AAAAAAA-B-C-D-E-F-G
362 | Description: Indicates the state of the given RFX sensor.
363 | Default Handler: **AlarmDecoder action button indicator**[#5](#flexible_handlers)
364 | -- capabilities [Contact sensor]
365 | -- actions [push: manually set state to off and clear]
366 | -- states [on(Active), off]
367 | -- preferences \[ invert:[true, false], zone: N/A]
368 |
369 | #1: The device name prefix is configurable during install by changing @Field lname at the top of the **_AlarmDecoder Service_** code. The name can be changed after installing but the address must not be modified.
370 |
371 | #2: **_AAA_** is the Contact ID number **_B_** is the partition and **_CCC_** is the zone or user code to match with '???' matching all. Ex. CID-401-012 will monitor user 012 arming/disarming. Supports regex in the deviceNetworkId to allow to create devices that can trigger on multiple CID messages such as **_"CID-4[0,4]]1-1-???"_** will monitor all users for arming/disarming away or stay on partition 1.
372 |
373 | #3:AAAAAA is the RF Serial Number B is battery status, C is supervisor event(ignore with ?), D is loop0(ignore with ?), E is loop1(ignore with ?), F is loop2(ignore with ?) and E is loop3(ignore with ?). Ex. RFX-123456-?-?-1-?-?-? will monitor 5800 RF sensor with serial number 123456 and loop1 for changes.
374 |
375 | #4: The number assigned initially to each device zone name is sequential and arbitrary. The actual zone tracked for each device is configured in the **AlarmDecoder UI** device settings page. So 'Security Zone Sensor #1' could actually be zone 20. Rename these as needed.
376 |
377 | #5: Each AlarmDecoder Virtual device receives a default type when it is created. This Device Type or Device Handler can be changed using the device editor. [Several example device types](#devicetypes) are provided and more can be created using the examples as reference.
378 |
379 |
380 |
381 | ## Alarm Decoder Device Handlers
382 |
383 | The Device Type or Device Handler for a given virtual device can be changed at any time. If the application requires a Smoke Detector and does not support Contact Sensors simply change the Type in the device editor. AlarmDecoder virtual devices will receive a translated message from on/off to a message appropriate to the devices type and capabilities.
384 |
385 | - AlarmDecoder virtual contact sensor
386 | capabilities [Contact Sensor]
387 | - AlarmDecoder action button indicator
388 | capabilities [Momentary, Switch]
389 | - AlarmDecoder status indicator
390 | capabilities [Contact Sensor]
391 | - AlarmDecoder virtual carbon monoxide detector
392 | capabilities [Carbon Monoxide Detector]
393 | - AlarmDecoder virtual shock sensor
394 | capabilities [Shock Sensor]
395 | - AlarmDecoder virtual motion sensor
396 | capabilities [Motion Sensor]
397 | - AlarmDecoder virtual smoke alarm
398 | capabilities [Smoke Detector]
399 |
400 | ## Known Issues
401 |
402 | - DSC:
403 | a. Extra zones will show up in the zone list.
404 | b. Arming STAY shows AWAY until after EXIT state.
405 | - ADEMCO:
406 | a. As with a regular keypad it is necessary to disarm a second time after an alarm to restore to Ready state. The Disarm button stays enabled when the panel is Not Ready.
407 | b. Fire state take 30 seconds to clear after being cleared on the panel.
408 | - All panels:
409 | a. not updating when the panel arms disarms etc.
410 | Subscription may have been lost during restart of web app.
411 | The AlarmDecoder SmartThings device will renew its subscription every 5 minutes.
412 | To force a renwal update the Settings such as the API KEY in the App or Device graph page.
413 |
--------------------------------------------------------------------------------
/devicetypes/alarmdecoder/alarmdecoder-action-button-indicator.src/alarmdecoder-action-button-indicator.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Virtual Momentary Switch Indicator for alarm panel
3 | *
4 | * Copyright 2016-2019 Nu Tech Software Solutions, Inc.
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not
7 | * use this file except in compliance with the License. You may obtain a copy
8 | * of the License at:
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | * License for the specific language governing permissions and limitations
15 | * under the License.
16 | *
17 | */
18 |
19 | /*
20 | * global support
21 | */
22 | import groovy.transform.Field
23 | @Field APPNAMESPACE = "alarmdecoder"
24 |
25 | metadata {
26 | definition(
27 | name: "AlarmDecoder action button indicator",
28 | namespace: APPNAMESPACE,
29 | author: "Nu Tech Software Solutions, Inc.") {
30 | capability "Switch"
31 | capability "Momentary"
32 | command "push"
33 | command "on"
34 | command "off"
35 | }
36 |
37 | // tile definitions
38 | tiles {
39 | standardTile(
40 | "switch",
41 | "device.switch",
42 | width: 2, height: 2,
43 | canChangeIcon: true) {
44 | state(
45 | "off",
46 | label: 'Push',
47 | action: "momentary.push",
48 | backgroundColor: "#ffffff")
49 | state(
50 | "on",
51 | label: 'Push',
52 | action: "momentary.push",
53 | backgroundColor: "#00a0dc")
54 | }
55 | main "switch"
56 | details "switch"
57 | }
58 |
59 | // preferences
60 | preferences {
61 | input(
62 | name: "invert",
63 | type: "bool",
64 | title: "Invert signal [true,false]",
65 | description: "Invert signal [true,false]." +
66 | " Changes ON/OFF,OPEN/CLOSE,DETECTED/CLEAR",
67 | required: false)
68 | input(
69 | name: "zone",
70 | type: "number",
71 | title: "Zone Number",
72 | description: "Zone # required for zone events.",
73 | required: false)
74 | }
75 |
76 | }
77 |
78 | /**
79 | * installed()/updated()
80 | *
81 | * It is not possible for a service to access preferences directly so
82 | * update device data value to allow access from parent
83 | * using getDeviceDataByName getDataValue
84 | * FIXME: diff ^ docs not clear.
85 | *
86 | */
87 | def installed() {
88 | updateDataValue("invert", invert.toString())
89 | updateDataValue("zone", zone.toString())
90 | }
91 |
92 | def updated() {
93 | updateDataValue("invert", invert.toString())
94 | updateDataValue("zone", zone.toString())
95 | }
96 |
97 | // Send the request to the service for processing.
98 | def push() {
99 | if (parent.debug)
100 | log.debug "AlarmDecoderActionButtonIndicator: Executing 'actionButton'"
101 | parent.actionButton(device.getDeviceNetworkId())
102 | }
103 |
104 | def on() {
105 | push()
106 | }
107 |
108 | def off() {
109 | push()
110 | }
111 |
--------------------------------------------------------------------------------
/devicetypes/alarmdecoder/alarmdecoder-network-appliance.src/alarmdecoder-network-appliance.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * AlarmDecoder Network Appliance
3 | *
4 | * Copyright 2016-2019 Nu Tech Software Solutions, Inc.
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not
7 | * use this file except in compliance with the License. You may obtain a copy
8 | * of the License at:
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | * License for the specific language governing permissions and limitations
15 | * under the License.
16 | *
17 | */
18 |
19 | /**
20 | * global support
21 | */
22 | import groovy.transform.Field
23 | @Field APPNAMESPACE = "alarmdecoder"
24 |
25 | /**
26 | * Parsing support
27 | */
28 | import groovy.json.JsonSlurper;
29 | import groovy.util.XmlParser;
30 |
31 | /**
32 | * System Settings
33 | */
34 | // The max number of zone faults we can show on this page.
35 | // To adjust requires manual adding of the tiles into preferences.
36 | // FIXME: refactor remove static count.
37 | @Field MAX_ZONE_FAULT_TILES = 12
38 |
39 | /**
40 | * preferences
41 | */
42 | preferences {
43 | section() {
44 | input(
45 | name: "api_key",
46 | type: "password",
47 | title: "API Key",
48 | description: "The key to access the REST API",
49 | required: true)
50 | input(
51 | name: "user_code",
52 | type: "password",
53 | title: "Alarm Code",
54 | description: "The user code for the panel",
55 | required: true)
56 | input(
57 | name: "panel_type",
58 | type: "enum",
59 | title: "Panel Type",
60 | description: "Type of panel [ADEMCO,DSC]",
61 | options: ["ADEMCO", "DSC"],
62 | defaultValue: "ADEMCO",
63 | required: true)
64 | }
65 | }
66 |
67 | /**
68 | * metadata
69 | */
70 | metadata {
71 | definition(
72 | name: "AlarmDecoder network appliance",
73 | namespace: APPNAMESPACE, author: "Nu Tech Software Solutions, Inc.") {
74 |
75 | // capabilities
76 | capability "Refresh"
77 |
78 | // attributes
79 | attribute(
80 | "panel_state",
81 | "enum",
82 | [
83 | "armed",
84 | "armed_stay",
85 | "armed_stay_exit",
86 | "disarmed",
87 | "alarming",
88 | "fire",
89 | "ready",
90 | "notready"
91 | ]
92 | )
93 | attribute "armed", "enum", ["armed", "disarmed"]
94 | attribute "panic_state", "string"
95 | attribute "zoneStatus1", "string"
96 | attribute "zoneStatus2", "string"
97 | attribute "zoneStatus3", "string"
98 | attribute "zoneStatus4", "string"
99 | attribute "zoneStatus5", "string"
100 | attribute "zoneStatus6", "string"
101 | attribute "zoneStatus7", "string"
102 | attribute "zoneStatus8", "string"
103 | attribute "zoneStatus9", "string"
104 | attribute "zoneStatus10", "string"
105 | attribute "zoneStatus11", "string"
106 | attribute "zoneStatus12", "string"
107 |
108 | // commands
109 | command "disarm"
110 | command "arm_stay"
111 | command "exit"
112 | command "arm_away"
113 | command "fire"
114 | command "fire1"
115 | command "fire2"
116 | command "panic"
117 | command "panic1"
118 | command "panic2"
119 | command "aux"
120 | command "aux1"
121 | command "aux2"
122 | command "chime"
123 | command "bypass", ["number"]
124 | command "bypassN", ["string"]
125 | command "bypass1"
126 | command "bypass2"
127 | command "bypass3"
128 | command "bypass4"
129 | command "bypass5"
130 | command "bypass6"
131 | command "bypass7"
132 | command "bypass8"
133 | command "bypass9"
134 | command "bypass10"
135 | command "bypass11"
136 | command "bypass12"
137 | }
138 |
139 | simulator {
140 | // TODO: define status and reply messages here
141 | }
142 |
143 | // tile definitions
144 | tiles(scale: 2) {
145 |
146 | // top alarm status box
147 | multiAttributeTile(
148 | name: "status",
149 | type: "generic",
150 | width: 6, height: 4) {
151 | tileAttribute(
152 | "device.panel_state",
153 | key: "PRIMARY_CONTROL") {
154 | attributeState(
155 | "armed",
156 | label: 'Armed',
157 | icon: "st.security.alarm.on",
158 | backgroundColor: "#ffa81e")
159 | attributeState(
160 | "armed_exit",
161 | label: 'Armed (exit-now)',
162 | icon: "st.nest.nest-away",
163 | backgroundColor:
164 | "#ffa81e")
165 | attributeState(
166 | "armed_stay",
167 | label: 'Armed (stay)',
168 | icon: "st.security.alarm.on",
169 | backgroundColor: "#ffa81e")
170 | attributeState(
171 | "armed_stay_exit",
172 | label: 'Armed (exit-now)',
173 | icon: "st.nest.nest-away",
174 | backgroundColor: "#ffa81e")
175 | attributeState(
176 | "disarmed",
177 | label: 'Disarmed',
178 | icon: "st.security.alarm.off",
179 | backgroundColor: "#79b821",
180 | defaultState: true)
181 | attributeState(
182 | "alarming",
183 | label: 'Alarming!',
184 | icon: "st.home.home2",
185 | backgroundColor: "#ff4000")
186 | attributeState(
187 | "fire",
188 | label: 'Fire!',
189 | icon: "st.contact.contact.closed",
190 | backgroundColor: "#ff0000")
191 | attributeState(
192 | "ready",
193 | label: 'Ready',
194 | icon: "st.security.alarm.off",
195 | backgroundColor: "#79b821")
196 | attributeState(
197 | "notready",
198 | label: 'Not Ready',
199 | icon: "st.security.alarm.off",
200 | backgroundColor: "#e86d13")
201 | }
202 | }
203 |
204 | // arm/disarm button #1
205 | standardTile(
206 | "arm_disarm",
207 | "device.panel_state",
208 | inactiveLabel: false,
209 | width: 2, height: 2) {
210 | state(
211 | "armed",
212 | action: "disarm",
213 | icon: "st.security.alarm.off",
214 | label: "DISARM")
215 | state(
216 | "armed_exit",
217 | action: "disarm",
218 | icon: "st.security.alarm.off",
219 | label: "DISARM")
220 | state(
221 | "armed_stay",
222 | action: "disarm",
223 | icon: "st.security.alarm.off",
224 | label: "DISARM")
225 | state(
226 | "armed_stay_exit",
227 | action: "disarm",
228 | icon: "st.security.alarm.off",
229 | label: "DISARM")
230 | state(
231 | "disarmed",
232 | action: "arm_away",
233 | icon: "st.security.alarm.on",
234 | label: "AWAY")
235 | state(
236 | "alarming",
237 | action: "disarm",
238 | icon: "st.security.alarm.off",
239 | label: "DISARM")
240 | state(
241 | "fire",
242 | action: "disarm",
243 | icon: "st.security.alarm.off",
244 | label: "DISARM")
245 | state(
246 | "ready",
247 | action: "arm_away",
248 | icon: "st.security.alarm.on",
249 | label: "AWAY")
250 | state(
251 | "notready",
252 | action: "disarm",
253 | icon: "st.security.alarm.off",
254 | label: "DISARM")
255 | }
256 |
257 | // arm/disarm button #2
258 | standardTile(
259 | "stay_disarm",
260 | "device.panel_state",
261 | inactiveLabel: false,
262 | width: 2, height: 2) {
263 | state(
264 | "armed",
265 | action: "disarm",
266 | icon: "st.security.alarm.off",
267 | label: "DISARM")
268 | state(
269 | "armed_exit",
270 | action: "disarm",
271 | icon: "st.security.alarm.off",
272 | label: "DISARM")
273 | state(
274 | "armed_stay",
275 | action: "exit",
276 | icon: "st.nest.nest-away",
277 | label: "EXIT")
278 | state(
279 | "armed_stay_exit",
280 | action: "disarm",
281 | icon: "st.security.alarm.off",
282 | label: "DISARM")
283 | state(
284 | "disarmed",
285 | action: "arm_stay",
286 | icon: "st.Home.home4",
287 | label: "STAY")
288 | state(
289 | "alarming",
290 | action: "disarm",
291 | icon: "st.security.alarm.off",
292 | label: "DISARM")
293 | state(
294 | "fire",
295 | action: "disarm",
296 | icon: "st.security.alarm.off",
297 | label: "DISARM")
298 | state(
299 | "ready",
300 | action: "arm_stay",
301 | icon: "st.security.alarm.on",
302 | label: "STAY")
303 | state(
304 | "notready",
305 | action: "disarm",
306 | icon: "st.security.alarm.off",
307 | label: "DISARM")
308 | }
309 |
310 | // panic buttons Touch 3 times to trigger an alarm.
311 | // panic police
312 | standardTile(
313 | "panic",
314 | "device.panic_state",
315 | inactiveLabel: false,
316 | width: 1, height: 1) {
317 | state(
318 | "default",
319 | icon: "http://www.alarmdecoder.com/st/ad2-police.png",
320 | label: "PANIC",
321 | nextState: "panic1",
322 | action: "panic1")
323 | state(
324 | "panic1",
325 | icon: "http://www.alarmdecoder.com/st/ad2-police.png",
326 | label: "PANIC",
327 | nextState: "panic2",
328 | action: "panic2",
329 | backgroundColor: "#ffa81e")
330 | state(
331 | "panic2",
332 | icon: "http://www.alarmdecoder.com/st/ad2-police.png",
333 | label: "PANIC", nextState: "default",
334 | action: "panic",
335 | backgroundColor: "#ff4000"
336 | )
337 | }
338 |
339 | // panic fire
340 | standardTile(
341 | "fire",
342 | "device.fire_state",
343 | inactiveLabel: false,
344 | width: 1, height: 1) {
345 | state(
346 | "default",
347 | icon: "http://www.alarmdecoder.com/st/ad2-fire.png",
348 | label: "FIRE",
349 | nextState: "fire1",
350 | action: "fire1")
351 | state(
352 | "fire1",
353 | icon: "http://www.alarmdecoder.com/st/ad2-fire.png",
354 | label: "FIRE",
355 | nextState: "fire2",
356 | action: "fire2",
357 | backgroundColor: "#ffa81e")
358 | state(
359 | "fire2",
360 | icon: "http://www.alarmdecoder.com/st/ad2-fire.png",
361 | label: "FIRE",
362 | nextState: "default",
363 | action: "fire",
364 | backgroundColor: "#ff4000")
365 | }
366 |
367 | // panic medical
368 | standardTile(
369 | "aux",
370 | "device.aux_state",
371 | inactiveLabel: false,
372 | width: 1, height: 1) {
373 | state(
374 | "default",
375 | icon: "http://www.alarmdecoder.com/st/ad2-aux.png",
376 | label: "AUX",
377 | nextState: "aux1",
378 | action: "aux1")
379 | state(
380 | "aux1",
381 | icon: "http://www.alarmdecoder.com/st/ad2-aux.png",
382 | label: "AUX",
383 | nextState: "aux2",
384 | action: "aux2",
385 | backgroundColor: "#ffa81e")
386 | state(
387 | "aux2",
388 | icon: "http://www.alarmdecoder.com/st/ad2-aux.png",
389 | label: "AUX",
390 | nextState: "default",
391 | action: "aux",
392 | backgroundColor: "#ff4000")
393 | }
394 |
395 | // zone fault tiles show a list of faulted zones where pressing will bypass.
396 | // #1
397 | valueTile(
398 | "zoneStatus1",
399 | "device.zoneStatus1",
400 | inactiveLabel: false,
401 | width: 1, height: 1) {
402 | state "default",
403 | icon: "",
404 | label: '${currentValue}',
405 | action: "bypass1",
406 | nextState: "default",
407 | backgroundColors: [
408 | [value: 0, color: "#ffffff"],
409 | [value: 1, color: "#ff0000"],
410 | [value: 99, color: "#ff0000"]
411 | ]
412 | }
413 | // #2
414 | valueTile(
415 | "zoneStatus2",
416 | "device.zoneStatus2",
417 | inactiveLabel: false,
418 | width: 1, height: 1) {
419 | state "default",
420 | icon: "",
421 | label: '${currentValue}',
422 | action: "bypass2",
423 | nextState: "default",
424 | backgroundColors: [
425 | [value: 0, color: "#ffffff"],
426 | [value: 1, color: "#ff0000"],
427 | [value: 99, color: "#ff0000"]
428 | ]
429 | }
430 | // #3
431 | valueTile(
432 | "zoneStatus3",
433 | "device.zoneStatus3",
434 | inactiveLabel: false,
435 | width: 1, height: 1) {
436 | state "default",
437 | icon: "",
438 | label: '${currentValue}',
439 | action: "bypass3",
440 | nextState: "default",
441 | backgroundColors: [
442 | [value: 0, color: "#ffffff"],
443 | [value: 1, color: "#ff0000"],
444 | [value: 99, color: "#ff0000"]
445 | ]
446 | }
447 | // #4
448 | valueTile(
449 | "zoneStatus4",
450 | "device.zoneStatus4",
451 | inactiveLabel: false,
452 | width: 1, height: 1) {
453 | state "default",
454 | icon: "",
455 | label: '${currentValue}',
456 | action: "bypass4",
457 | nextState: "default",
458 | backgroundColors: [
459 | [value: 0, color: "#ffffff"],
460 | [value: 1, color: "#ff0000"],
461 | [value: 99, color: "#ff0000"]
462 | ]
463 | }
464 | // #5
465 | valueTile(
466 | "zoneStatus5",
467 | "device.zoneStatus5",
468 | inactiveLabel: false,
469 | width: 1, height: 1) {
470 | state "default",
471 | icon: "",
472 | label: '${currentValue}',
473 | action: "bypass5",
474 | nextState: "default",
475 | backgroundColors: [
476 | [value: 0, color: "#ffffff"],
477 | [value: 1, color: "#ff0000"],
478 | [value: 99, color: "#ff0000"]
479 | ]
480 | }
481 | // #6
482 | valueTile(
483 | "zoneStatus6",
484 | "device.zoneStatus6",
485 | inactiveLabel: false,
486 | width: 1, height: 1) {
487 | state "default",
488 | icon: "",
489 | label: '${currentValue}',
490 | action: "bypass6",
491 | nextState: "default",
492 | backgroundColors: [
493 | [value: 0, color: "#ffffff"],
494 | [value: 1, color: "#ff0000"],
495 | [value: 99, color: "#ff0000"]
496 | ]
497 | }
498 | // #7
499 | valueTile(
500 | "zoneStatus7",
501 | "device.zoneStatus7",
502 | inactiveLabel: false,
503 | width: 1, height: 1) {
504 | state "default",
505 | icon: "",
506 | label: '${currentValue}',
507 | action: "bypass7",
508 | nextState: "default",
509 | backgroundColors: [
510 | [value: 0, color: "#ffffff"],
511 | [value: 1, color: "#ff0000"],
512 | [value: 99, color: "#ff0000"]
513 | ]
514 | }
515 | // #8
516 | valueTile(
517 | "zoneStatus8",
518 | "device.zoneStatus8",
519 | inactiveLabel: false,
520 | width: 1, height: 1) {
521 | state "default",
522 | icon: "",
523 | label: '${currentValue}',
524 | action: "bypass8",
525 | nextState: "default",
526 | backgroundColors: [
527 | [value: 0, color: "#ffffff"],
528 | [value: 1, color: "#ff0000"],
529 | [value: 99, color: "#ff0000"]
530 | ]
531 | }
532 | // #9
533 | valueTile(
534 | "zoneStatus9",
535 | "device.zoneStatus9",
536 | inactiveLabel: false,
537 | width: 1, height: 1) {
538 | state "default",
539 | icon: "",
540 | label: '${currentValue}',
541 | action: "bypass9",
542 | nextState: "default",
543 | backgroundColors: [
544 | [value: 0, color: "#ffffff"],
545 | [value: 1, color: "#ff0000"],
546 | [value: 99, color: "#ff0000"]
547 | ]
548 | }
549 | // #10
550 | valueTile(
551 | "zoneStatus10",
552 | "device.zoneStatus10",
553 | inactiveLabel: false,
554 | width: 1, height: 1) {
555 | state "default",
556 | icon: "",
557 | label: '${currentValue}',
558 | action: "bypass10",
559 | nextState: "default",
560 | backgroundColors: [
561 | [value: 0, color: "#ffffff"],
562 | [value: 1, color: "#ff0000"],
563 | [value: 99, color: "#ff0000"]
564 | ]
565 | }
566 | // #11
567 | valueTile(
568 | "zoneStatus11",
569 | "device.zoneStatus11",
570 | inactiveLabel: false,
571 | width: 1, height: 1) {
572 | state "default",
573 | icon: "",
574 | label: '${currentValue}',
575 | action: "bypass11",
576 | nextState: "default",
577 | backgroundColors: [
578 | [value: 0, color: "#ffffff"],
579 | [value: 1, color: "#ff0000"],
580 | [value: 99, color: "#ff0000"]
581 | ]
582 | }
583 | // #12
584 | valueTile(
585 | "zoneStatus12",
586 | "device.zoneStatus12",
587 | inactiveLabel: false,
588 | width: 1, height: 1) {
589 | state "default",
590 | icon: "",
591 | label: '${currentValue}',
592 | action: "bypass12",
593 | nextState: "default",
594 | backgroundColors: [
595 | [value: 0, color: "#ffffff"],
596 | [value: 1, color: "#ff0000"],
597 | [value: 99, color: "#ff0000"]
598 | ]
599 | }
600 |
601 | // refresh button (PULL results from server)
602 | // FIXME: Not need as things improve. Make smaller still useful for testing.
603 | standardTile(
604 | "refresh",
605 | "device.refresh",
606 | inactiveLabel: false,
607 | decoration: "flat",
608 | width: 1, height: 1) {
609 | state "default",
610 | action: "refresh.refresh",
611 | icon: "st.secondary.refresh",
612 | label: "refresh"
613 | }
614 |
615 | // main page layout and order
616 | main(["status"])
617 | details(
618 | ["status",
619 | "arm_disarm",
620 | "stay_disarm",
621 | "panic",
622 | "fire",
623 | "aux",
624 | "refresh",
625 | "zoneStatus1",
626 | "zoneStatus2",
627 | "zoneStatus3",
628 | "zoneStatus4",
629 | "zoneStatus5",
630 | "zoneStatus6",
631 | "zoneStatus7",
632 | "zoneStatus8",
633 | "zoneStatus9",
634 | "zoneStatus10",
635 | "zoneStatus11",
636 | "zoneStatus12"
637 | ])
638 | }
639 | }
640 |
641 |
642 | /*** Event Handlers ***/
643 |
644 | /**
645 | * installed()
646 | *
647 | * Called when the device page reports an install event.
648 | */
649 | def installed() {
650 | // FIXME: refactor remove for() with fixed #
651 | for (def i = 1; i <= 12; i++)
652 | sendEvent(name: "zoneStatus${i}", value: "", displayed: false)
653 | }
654 |
655 | /**
656 | * uninstalled()
657 | *
658 | * Called when the device page reports an uninstall event.
659 | */
660 | def uninstalled() {
661 | log.trace "--- handler.uninstalled"
662 | }
663 |
664 | /**
665 | * update()
666 | *
667 | * Called when the device settings are udpated.
668 | */
669 | def updated() {
670 | log.trace "--- handler.updated"
671 |
672 | state.faulted_zones = []
673 |
674 | // Raw panel state values
675 | state.panel_ready = true
676 | state.panel_armed = false
677 | state.panel_armed_stay = false
678 | state.panel_exit = false
679 | state.panel_fire_detected = false
680 | state.panel_alarming = false
681 | state.panel_panicked = false
682 | state.panel_on_battery = true
683 | state.panel_powered = true
684 | state.panel_chime = true
685 | state.panel_perimeter_only = false
686 | state.panel_entry_delay_off = false
687 |
688 | // Calculated alarm state ENUM
689 | state.panel_state = "disarmed"
690 | state.alarm_status = "off"
691 | state.armed = false
692 |
693 | // internal state vars
694 | state.panic_started = null;
695 | state.fire_started = null;
696 | state.aux_started = null;
697 |
698 | // FIXME: refactor remove for() with fixed #
699 | for (def i = 1; i <= 12; i++)
700 | sendEvent(name: "zoneStatus${i}", value: "", displayed: false)
701 |
702 | // subscribe if settings change
703 | unschedule()
704 | // subscribe right now
705 | subscribeNotifications()
706 | // resub every 5min
707 | runEvery5Minutes(subscribeNotifications)
708 | }
709 |
710 | /**
711 | * parse()
712 | *
713 | * Core event parse routine for Device Handler
714 | *
715 | * Expect:
716 | * Response to SUBSCRIBE requests to the AlarmDecoder.
717 | *
718 | * Test from the AlarmDecoder Appliance:
719 | * curl
720 | *
721 | */
722 | def parse(String description) {
723 | if (parent.debug)
724 | log.debug("--- parse: em: ${em}, description: '${description}'")
725 |
726 | // Create our events array we will append into.
727 | def events = []
728 |
729 | // Parse the event string.
730 | def em = parent.parseEventMessage(description)
731 |
732 | // Is this a message from the active registerd AlarmDecoder?
733 | // FIXME: Add some security test.
734 | if (em.mac != getDataValue("mac")) {
735 | if (parent.debug)
736 | log.info("--- parse: skipping event incorrect mac address: ${em.mac}")
737 | return events
738 | } else {
739 | if (parent.debug)
740 | log.info("--- parse: event match mac: ${em.mac}")
741 | }
742 |
743 | // If we initiate a connection it will get an id.
744 | // Just grab the last for loging.
745 | def rID = ((em.requestId?.length() > 12) ?
746 | em.requestId.substring(em.requestId.length() - 12) : "000000000000")
747 |
748 | // We need headers for a valid request.
749 | if (!em.headers) {
750 | if (parent.debug)
751 | log.info("--- parse: skipping event missing headers from ${em.mac}")
752 | return events
753 | }
754 |
755 | log.info("--- parse: mac: ${event?.mac} requestId: ${rID}")
756 |
757 | // The body may be empty.
758 | def bodyString = (em.body) ? em.body : ""
759 |
760 | // Verbose debug details.
761 | if (parent.debug) {
762 | log.debug("--- parse: type: ${em.contenttype} " +
763 | "rid: ${em.requestId}, headers: ${em.headers}")
764 | log.debug("--- parse: rid: ${rID}, body: ${em.body}")
765 | }
766 |
767 | def type = em.contenttype
768 |
769 | // Based upon content type process the content.
770 | if (type?.contains("json") && bodyString.length()) {
771 | parse_json(bodyString).each {
772 | e-> events << e
773 | }
774 | } else if (type?.contains("xml") && bodyString.length()) {
775 | parse_xml(bodyString).each {
776 | e-> events << e
777 | }
778 | } else {
779 | if (parent.debug) log.debug("--- parse: unknown type: ${type}")
780 | }
781 |
782 | if (parent.debug)
783 | log.debug("--- parse: results rid:${rID}, events: ${events}")
784 |
785 | return events
786 | }
787 |
788 |
789 |
790 | /*** Capabilities ***/
791 |
792 | def refresh() {
793 | log.trace("--- handler.refresh")
794 |
795 | def urn = getDataValue("urn")
796 | def apikey = _get_api_key()
797 |
798 | return hub_http_get(urn, "/api/v1/alarmdecoder?apikey=${apikey}")
799 | }
800 |
801 | /*** Commands ***/
802 | /**
803 | * disarm()
804 | * Sends a disarm command to the panel
805 | * TODO: Add security
806 | */
807 | def disarm() {
808 | log.trace("--- disarm")
809 |
810 | def user_code = _get_user_code()
811 | def keys = ""
812 |
813 | if (settings.panel_type == "ADEMCO")
814 | keys = "${user_code}1"
815 | else if (settings.panel_type == "DSC")
816 | keys = "${user_code}"
817 | else
818 | log.warn("--- disarm: unknown panel_type.")
819 |
820 | return send_keys(keys)
821 | }
822 |
823 | /**
824 | * exit()
825 | * Sends a exit command to the panel
826 | * TODO: Add security
827 | */
828 | def exit() {
829 | log.trace("--- exit")
830 |
831 | def user_code = _get_user_code()
832 | def keys = ""
833 |
834 | if (settings.panel_type == "ADEMCO")
835 | keys = "*"
836 | else if (settings.panel_type == "DSC")
837 | keys = "S8"
838 | else
839 | log.warn("--- exit: unknown panel_type.")
840 |
841 | return send_keys(keys)
842 | }
843 |
844 | /**
845 | * arm_away()
846 | * Sends an arm away command to the panel
847 | */
848 | def arm_away() {
849 | log.trace("--- arm_away")
850 |
851 | def user_code = _get_user_code()
852 | def keys = ""
853 |
854 | if (settings.panel_type == "ADEMCO")
855 | keys = "${user_code}2"
856 | else if (settings.panel_type == "DSC")
857 | keys = ""
858 | else
859 | log.warn("--- arm_away: unknown panel_type.")
860 |
861 | return send_keys(keys)
862 | }
863 |
864 | /**
865 | * arm_stay()
866 | * Sends an arm stay command to the panel
867 | */
868 | def arm_stay() {
869 | log.trace("--- arm_stay")
870 |
871 | def user_code = _get_user_code()
872 | def keys = ""
873 |
874 | if (settings.panel_type == "ADEMCO")
875 | keys = "${user_code}3"
876 | else if (settings.panel_type == "DSC")
877 | keys = ""
878 | else
879 | log.warn("--- arm_stay: unknown panel_type.")
880 |
881 | return send_keys(keys)
882 | }
883 |
884 | /**
885 | * fire()
886 | * Sends an fire alarm command to the panel
887 | */
888 | def fire() {
889 | log.trace("--- fire")
890 | state.fire_started = null
891 | def keys = ""
892 | return send_keys(keys)
893 | }
894 |
895 | def fire1() {
896 | state.fire_started = new Date().time
897 |
898 | runIn(10, checkFire);
899 |
900 | log.trace("Fire stage 1: ${state.fire_started}")
901 | }
902 |
903 | def fire2() {
904 | state.fire_started = new Date().time
905 |
906 | runIn(10, checkFire);
907 |
908 | log.trace("Fire stage 2: ${state.fire_started}")
909 | }
910 |
911 | def checkFire() {
912 | log.trace("checkFire");
913 | if (state.fire_started != null && new Date().time - state.fire_started >= 5) {
914 | sendEvent(name: "fire_state", value: "default", isStateChange: true);
915 | log.trace("clearing fire");
916 | }
917 | }
918 |
919 | /**
920 | * panic()
921 | * Sends an panic alarm command to the panel
922 | */
923 | def panic() {
924 | log.trace("--- panic")
925 | state.panic_started = null
926 | def keys = ""
927 | return send_keys(keys)
928 | }
929 |
930 | def panic1() {
931 | state.panic_started = new Date().time
932 |
933 | runIn(10, checkPanic);
934 |
935 | log.trace("Panic stage 1: ${state.panic_started}")
936 | }
937 |
938 | def panic2() {
939 | state.panic_started = new Date().time
940 |
941 | runIn(10, checkPanic);
942 |
943 | log.trace("Panic stage 2: ${state.panic_started}")
944 | }
945 |
946 | def checkPanic() {
947 | log.trace("checkPanic");
948 | if (state.panic_started != null &&
949 | new Date().time - state.panic_started >= 5) {
950 | sendEvent(name: "panic_state", value: "default", isStateChange: true);
951 | log.trace("clearing panic");
952 | }
953 | }
954 |
955 | /**
956 | * aux()
957 | * Sends an aux alarm command to the panel
958 | */
959 | def aux() {
960 | log.trace("--- aux")
961 | state.aux_started = null
962 | def keys = ""
963 | return send_keys(keys)
964 | }
965 |
966 | def aux1() {
967 | state.aux_started = new Date().time
968 |
969 | runIn(10, checkAux);
970 |
971 | log.trace("Aux stage 1: ${state.aux_started}")
972 | }
973 |
974 | def aux2() {
975 | state.aux_started = new Date().time
976 |
977 | runIn(10, checkAux);
978 |
979 | log.trace("Aux stage 2: ${state.aux_started}")
980 | }
981 |
982 | def checkAux() {
983 | log.trace("checkAux");
984 | if (state.aux_started != null && new Date().time - state.aux_started >= 5) {
985 | sendEvent(name: "aux_state", value: "default", isStateChange: true);
986 | log.trace("clearing aux");
987 | }
988 | }
989 |
990 | /**
991 | * bypassX()
992 | *
993 | */
994 | def bypass1(evt) { bypassN(1) }
995 | def bypass2(evt) { bypassN(2) }
996 | def bypass3(evt) { bypassN(3) }
997 | def bypass4(evt) { bypassN(4) }
998 | def bypass5(evt) { bypassN(5) }
999 | def bypass6(evt) { bypassN(6) }
1000 | def bypass7(evt) { bypassN(7) }
1001 | def bypass8(evt) { bypassN(8) }
1002 | def bypass9(evt) { bypassN(9) }
1003 | def bypass10(evt) { bypassN(10) }
1004 | def bypass11(evt) { bypassN(11) }
1005 | def bypass12(evt) { bypassN(12) }
1006 |
1007 | /**
1008 | * bypassN()
1009 | * Bypass the zone indicated on this tile
1010 | */
1011 | def bypassN(szValue) {
1012 | def zone = device.currentValue("zoneStatus${szValue}")
1013 | bypass(zone)
1014 | }
1015 |
1016 | /**
1017 | * bypass()
1018 | * Send a bypass command to the panel for a zone number.
1019 | */
1020 | def bypass(zone) {
1021 | log.trace("--- bypass ${zone}")
1022 |
1023 | // if no zone then skip
1024 | if (!zone.toInteger())
1025 | return;
1026 |
1027 | def user_code = _get_user_code()
1028 | def keys = ""
1029 |
1030 | if (settings.panel_type == "ADEMCO")
1031 | keys = "${user_code}6" + zone.toString().padLeft(2, "0") + "*"
1032 | else if (settings.panel_type == "DSC")
1033 | keys = "*1" + zone.toString().padLeft(2, "0") + "#"
1034 | else
1035 | log.warn("--- bypass: unknown panel_type.")
1036 |
1037 | return send_keys(keys)
1038 | }
1039 |
1040 | /**
1041 | * chime()
1042 | * Sends a chime command to the panel
1043 | */
1044 | def chime() {
1045 | log.trace("--- chime")
1046 | def user_code = _get_user_code()
1047 | def keys = ""
1048 |
1049 | if (settings.panel_type == "ADEMCO")
1050 | keys = "${user_code}9*"
1051 | else if (settings.panel_type == "DSC")
1052 | keys = ""
1053 | else
1054 | log.warn("--- chime: unknown panel_type.")
1055 |
1056 | return send_keys(keys)
1057 | }
1058 |
1059 | /*** Business Logic ***/
1060 |
1061 | /**
1062 | * update_state(data)
1063 | * process a state change event object from xml/json parser
1064 | */
1065 | def update_state(data) {
1066 | log.trace("--- update_state")
1067 | def forceguiUpdate = false
1068 | def skipstate = false
1069 | def events = []
1070 |
1071 | /*
1072 | * WARNING: we can get the Ready and Armed state events out of
1073 | * order thanks to async io and the fact that both events occure
1074 | * from a single AD2 message.Handle this gracefully to avoid client
1075 | * update glitches.
1076 | *
1077 | * [10000001000100003A0-],008,[f702...],"****DISARMED**** Ready to Arm "
1078 | * [00100301000100003A0-],008,[f702...],"ARMED ***STAY***You may exit now"
1079 | * [10000101000100003A0-],008,[f702...],"****DISARMED**** Ready to Arm "
1080 | */
1081 |
1082 | // Event Type 14 CID send raw data upstream if we find one
1083 | if (data.eventid == 14) {
1084 | events << createEvent(
1085 | name: "cid-set",
1086 | value: data.rawmessage,
1087 | displayed: true,
1088 | isStateChange: true)
1089 |
1090 | // do not trust state for these messages they could be out of sync.
1091 | skipstate = true;
1092 | }
1093 |
1094 | // Event Type 17 RFX send eventmessage upstream if we find one
1095 | if (data.eventid == 17) {
1096 | events << createEvent(
1097 | name: "rfx-set",
1098 | value: data.eventmessage,
1099 | displayed: true,
1100 | isStateChange: true)
1101 | // do not trust state for these messages they could be out of sync.
1102 | skipstate = true;
1103 | }
1104 |
1105 | // Event Type 18 EXP/REL send eventmessage upstream if we find one
1106 | if (data.eventid == 18) {
1107 | events << createEvent(
1108 | name: "exp-set",
1109 | value: data.eventmessage,
1110 | displayed: true,
1111 | isStateChange: true)
1112 | // do not trust state for these messages they could be out of sync.
1113 | skipstate = true;
1114 | }
1115 |
1116 | // Event Type 19 AUI send eventmessage upstream if we find one
1117 | if (data.eventid == 19) {
1118 | events << createEvent(
1119 | name: "aui-set",
1120 | value: data.eventmessage,
1121 | displayed: true,
1122 | isStateChange: true)
1123 | // do not trust state for these messages they could be out of sync.
1124 | skipstate = true;
1125 | }
1126 |
1127 | // Event Type 5 Bypass
1128 | if (data.eventid == 5) {
1129 | events << createEvent(
1130 | name: "bypass-set",
1131 | value: (data.panel_bypassed ? "on" : "off"),
1132 | displayed: true,
1133 | isStateChange: true)
1134 | // do not trust state for these messages they could be out of sync.
1135 | skipstate = true;
1136 | }
1137 |
1138 | // Event Type 16 Chime
1139 | if (data.eventid == 16) {
1140 | events << createEvent(
1141 | name: "chime-set",
1142 | value: (data.panel_chime ? "on" : "off"),
1143 | displayed: true,
1144 | isStateChange: true)
1145 | // do not trust state for these messages they could be out of sync.
1146 | skipstate = true;
1147 | }
1148 |
1149 | // SKIP if needed
1150 | if (skipstate != true) {
1151 | // Get our armed state from our new state data
1152 | def armed = data.panel_armed ||
1153 | (data.panel_armed_stay != null && data.panel_armed_stay == true)
1154 |
1155 | def panel_state = (data.panel_ready ? "ready" : "notready")
1156 |
1157 | // Update our ready status virtual device
1158 | if (forceguiUpdate || data.panel_ready != state.panel_ready)
1159 | events << createEvent(
1160 | name: "ready-set",
1161 | value: (data.panel_ready ? "on" : "off"),
1162 | displayed: true,
1163 | isStateChange: true)
1164 |
1165 | // If armed update internal UI state to exit mode and armed state
1166 | if (armed) {
1167 | if (data.panel_exit) {
1168 | if (data.panel_armed_stay) {
1169 | panel_state = "armed_stay_exit"
1170 | } else {
1171 | panel_state = "armed_exit"
1172 | }
1173 | } else {
1174 | panel_state = (data.panel_armed_stay ? "armed_stay" : "armed")
1175 | }
1176 | }
1177 |
1178 | // FORCE ARMED if ALARMING to be sure MONITOR gets it as it will
1179 | // not show alarms if not armed :(
1180 | if (data.panel_alarming) {
1181 | panel_state = "alarming"
1182 | }
1183 |
1184 | // NOTE: Fire overrides alarm since it's definitely more serious.
1185 | if (data.panel_fire_detected) {
1186 | panel_state = "fire"
1187 | }
1188 |
1189 | // Update our Smoke Sensor virtual device that MONITOR or
1190 | // others the current state.
1191 | if (forceguiUpdate || data.panel_fire_detected != state.panel_fire_detected)
1192 | events << createEvent(
1193 | name: "smoke-set",
1194 | value: (data.panel_fire_detected ? "detected" : "clear"),
1195 | displayed: true,
1196 | isStateChange: true)
1197 |
1198 | // If armed STAY changes data.panel_armed_stay
1199 | if (forceguiUpdate || data.panel_armed_stay != state.panel_armed_stay) {
1200 | if (data.panel_armed_stay) {
1201 | events << createEvent(
1202 | name: "arm-stay-set",
1203 | value: "on",
1204 | displayed: true,
1205 | isStateChange: true)
1206 | events << createEvent(
1207 | name: "arm-away-set",
1208 | value: "off",
1209 | displayed: true,
1210 | isStateChange: true)
1211 | }
1212 | }
1213 |
1214 | // If the panel ARMED state changes
1215 | if (forceguiUpdate || armed != state.armed) {
1216 | if (!armed) {
1217 | events << createEvent(
1218 | name: "arm-away-set",
1219 | value: "off",
1220 | displayed: true,
1221 | isStateChange: true)
1222 | events << createEvent(
1223 | name: "arm-stay-set",
1224 | value: "off",
1225 | displayed: true,
1226 | isStateChange: true)
1227 | events << createEvent(
1228 | name: "disarm-set",
1229 | value: "off",
1230 | displayed: true,
1231 | isStateChange: true)
1232 | } else {
1233 | // If armed AWAY changes data.panel_armed_away
1234 | if (!data.panel_armed_stay)
1235 | events << createEvent(
1236 | name: "arm-away-set",
1237 | value: "on",
1238 | displayed: true,
1239 | isStateChange: true)
1240 | events << createEvent(
1241 | name: "disarm-set",
1242 | value: "on",
1243 | displayed: true,
1244 | isStateChange: true)
1245 | }
1246 | }
1247 |
1248 | // If Update exit state
1249 | if (forceguiUpdate || data.panel_exit != state.panel_exit) {
1250 | events << createEvent(
1251 | name: "exit-set",
1252 | value: (data.panel_exit ? "on" : "off"),
1253 | displayed: true,
1254 | isStateChange: true)
1255 | }
1256 |
1257 | // If Update perimeter only device
1258 | if (forceguiUpdate ||
1259 | data.panel_perimeter_only != state.panel_perimeter_only) {
1260 | events << createEvent(
1261 | name: "perimeter-only-set",
1262 | value: (data.panel_perimeter_only ? "on" : "off"),
1263 | displayed: true,
1264 | isStateChange: true)
1265 | }
1266 |
1267 | // If Update entry delay off device
1268 | if (forceguiUpdate ||
1269 | data.panel_entry_delay_off != state.panel_entry_delay_off) {
1270 | events << createEvent(
1271 | name: "entry-delay-off-set",
1272 | value: (data.panel_entry_delay_off ? "on" : "off"),
1273 | displayed: true,
1274 | isStateChange: true)
1275 | }
1276 |
1277 | // set our panel_state
1278 | if (forceguiUpdate || panel_state != state.panel_state) {
1279 | log.trace("--- update_state: new state **** ${panel_state} ****")
1280 | events << createEvent(
1281 | name: "panel_state",
1282 | value: panel_state,
1283 | displayed: true,
1284 | isStateChange: true)
1285 | }
1286 |
1287 | // build our alarm_status value
1288 | def alarm_status = "off"
1289 | if (armed) {
1290 | alarm_status = "away"
1291 | if (data.panel_armed_stay == true)
1292 | alarm_status = "stay"
1293 | }
1294 |
1295 | // Create an event to notify Smart Home Monitor in our service.
1296 | // "enum", ["off", "stay", "away"]
1297 | if (forceguiUpdate || alarm_status != state.alarm_status)
1298 | events << createEvent(
1299 | name: "alarmStatus",
1300 | value: alarm_status,
1301 | displayed: true,
1302 | isStateChange: true)
1303 |
1304 | // Update our alarming switch so MONITORs know we are in an alarm state.
1305 | // In alarm close contact.
1306 | // "enum", ["open", "closed"]
1307 | if (forceguiUpdate || data.panel_alarming != state.panel_alarming)
1308 | events << createEvent(
1309 | name: "alarmbell-set",
1310 | value: (data.panel_alarming ? "on" : "off"),
1311 | displayed: true,
1312 | isStateChange: true)
1313 |
1314 | // will only add events for zones that change state.
1315 | def zone_events = build_zone_events(data)
1316 | events = events.plus(zone_events)
1317 |
1318 | // Update our saved state
1319 | /// Calculated state enum
1320 | state.panel_state = panel_state
1321 | state.armed = armed
1322 |
1323 | /// raw panel state bits
1324 | state.panel_ready = data.panel_ready
1325 | state.panel_armed = data.panel_armed
1326 | state.panel_armed_stay = data.panel_armed_stay
1327 | state.panel_exit = data.panel_exit
1328 | state.panel_fire_detected = data.panel_fire_detected
1329 | state.panel_alarming = data.panel_alarming
1330 | state.alarm_status = alarm_status
1331 | state.panel_powered = data.panel_powered
1332 | state.panel_on_battery = data.panel_on_battery
1333 | state.panel_ready = data.panel_ready
1334 | state.panel_chime = data.chime
1335 | state.panel_perimeter_only = data.panel_perimeter_only
1336 | state.panel_entry_delay_off = data.panel_entry_delay_off
1337 | }
1338 | return events
1339 | }
1340 |
1341 | /*** Utility ***/
1342 |
1343 | /**
1344 | * parse_json(String body)
1345 | *
1346 | * Parse json response data from a PULL request made to the
1347 | * AlarmDecoder REST api into an event object and
1348 | * send to update_state().
1349 | */
1350 | def parse_json(String body) {
1351 | def events = []
1352 |
1353 | try {
1354 | def slurper = new JsonSlurper()
1355 | def result = slurper.parseText(body)
1356 |
1357 | // Build our events list from our current state
1358 | update_state(result).each {
1359 | e-> events << e
1360 | }
1361 |
1362 | if (parent.debug) log.debug("parse_json in:****** ${resultMap}")
1363 | if (parent.debug) log.debug("parse_json out:****** ${events}")
1364 |
1365 | } catch (Exception e) {
1366 | log.error("parse_json: Exception ${e}")
1367 | }
1368 |
1369 | return events
1370 | }
1371 |
1372 | /**
1373 | * parse_xml(String body)
1374 | *
1375 | * Parse xml data from a UPNP PUSH message from the AlarmDecoder
1376 | * push notification service into an event object and
1377 | * send to update_state().
1378 | */
1379 | def parse_xml(String body) {
1380 | def events = []
1381 |
1382 | try {
1383 | def xmlResult = new XmlSlurper().parseText(body)
1384 |
1385 | def resultMap = [: ]
1386 | resultMap['eventid'] =
1387 | xmlResult.property.eventid.toInteger()
1388 | resultMap['eventdesc'] =
1389 | xmlResult.property.eventdesc.text()
1390 | resultMap['eventmessage'] =
1391 | xmlResult.property.eventmessage.text()
1392 | resultMap['rawmessage'] =
1393 | xmlResult.property.rawmessage.text()
1394 | resultMap['last_message_received'] =
1395 | xmlResult.property.panelstate.last_message_received.text()
1396 | resultMap['panel_alarming'] =
1397 | xmlResult.property.panelstate.panel_alarming.toBoolean()
1398 | resultMap['panel_armed'] =
1399 | xmlResult.property.panelstate.panel_armed.toBoolean()
1400 | resultMap['panel_armed_stay'] =
1401 | xmlResult.property.panelstate.panel_armed_stay.toBoolean()
1402 | resultMap['panel_exit'] =
1403 | xmlResult.property.panelstate.panel_exit.toBoolean()
1404 | resultMap['panel_bypassed'] =
1405 | xmlResult.property.panelstate.panel_bypassed.toBoolean()
1406 | resultMap['panel_fire_detected'] =
1407 | xmlResult.property.panelstate.panel_fire_detected.toBoolean()
1408 | resultMap['panel_on_battery'] =
1409 | xmlResult.property.panelstate.panel_on_battery.toBoolean()
1410 | resultMap['panel_panicked'] =
1411 | xmlResult.property.panelstate.panel_panicked.toBoolean()
1412 | resultMap['panel_powered'] =
1413 | xmlResult.property.panelstate.panel_powered.toBoolean()
1414 | resultMap['panel_ready'] =
1415 | xmlResult.property.panelstate.panel_ready.toBoolean()
1416 | resultMap['panel_entry_delay_off'] =
1417 | xmlResult.property.panelstate.panel_entry_delay_off.toBoolean()
1418 | resultMap['panel_perimeter_only'] =
1419 | xmlResult.property.panelstate.panel_perimeter_only.toBoolean()
1420 | resultMap['panel_type'] =
1421 | xmlResult.property.panelstate.panel_type.text()
1422 | resultMap['panel_chime'] =
1423 | xmlResult.property.panelstate.panel_chime.toBoolean()
1424 |
1425 | // build list of faulted zones unpack xml
1426 | // only update zone list on zone change events
1427 | //
1428 | // (9) has been faulted.]]>
1429 | //
1430 | if (xmlResult.property.eventmessage.text().startsWith('Zone ')) {
1431 | def zones = []
1432 | xmlResult.property.panelstate.panel_zones_faulted.z.each {
1433 | e-> zones << e.toInteger()
1434 | }
1435 | resultMap['panel_zones_faulted'] = zones
1436 | }
1437 |
1438 | // unpack the relay xml
1439 | def relays = []
1440 | xmlResult.property.panelstate.panel_relay_status.r.each {
1441 | e->
1442 | relays << ['address': e.a, 'channel': e.c, 'value': e.v]
1443 | }
1444 | resultMap['panel_relay_status'] = relays
1445 |
1446 | // Build our events list from our current state
1447 | update_state(resultMap).each {
1448 | e-> events << e
1449 | }
1450 |
1451 | if (parent.debug) log.debug("parse_xml in:****** ${resultMap}")
1452 | if (parent.debug) log.debug("parse_xml out:****** ${events}")
1453 |
1454 | } catch (Exception e) {
1455 | log.error("parse_xml: Exception ${e}")
1456 | }
1457 |
1458 | return events
1459 | }
1460 |
1461 | /**
1462 | * subscribeNotifications()
1463 | *
1464 | * Send a SUBSCRIBE request to the AlarmDecoder UPNP notification service.
1465 | * To receive event PUSH notifications.
1466 | *
1467 | * Note:
1468 | * Be sure to enable the AlarmDecoder UPNP notification service.
1469 | *
1470 | * FIXME: Need to get this from the eventSubURL in the
1471 | * ssdpPath: /static/device_description.xml
1472 | */
1473 | def subscribeNotifications() {
1474 | if (parent.debug)
1475 | log.trace "--- subscribeNotifications: ${getDataValue("urn")}"
1476 |
1477 | // Get our HUBs address details for callbacks.
1478 | def address = parent.getHubURN()
1479 |
1480 | // Build the SUBSCRIBE request.
1481 | def obj = [
1482 | method: "SUBSCRIBE",
1483 | path: "/api/v1/alarmdecoder/event?apikey=${_get_api_key()}",
1484 | headers: [
1485 | HOST: getDataValue("urn"),
1486 | CALLBACK: "",
1487 | NT: "upnp:event",
1488 | TIMEOUT: "Second-28800"
1489 | ]
1490 | ]
1491 |
1492 | // Build the HubAction object.
1493 | def ha = parent.getHubAction(obj, address)
1494 |
1495 | // Tag the HubAction message. It will return in the event handler parse().
1496 | // FIXME: Add some security test.
1497 | if (parent.isSmartThings())
1498 | ha.requestId = "SUBSCRIBE"
1499 |
1500 | sendHubCommand(ha)
1501 | }
1502 |
1503 |
1504 | /**
1505 | * build_zone_events(data)
1506 | *
1507 | * Parse the zone events and return a list of events
1508 | * for each zone filtered to report only events with
1509 | * a changes in state.
1510 | */
1511 | private def build_zone_events(data) {
1512 | def events = []
1513 |
1514 | // TODO: probably remove this.
1515 | if (state.faulted_zones == null)
1516 | state.faulted_zones = []
1517 |
1518 | // If we have no tag then do nothing.
1519 | def current_faults = data.panel_zones_faulted
1520 | if (current_faults == null)
1521 | return events
1522 |
1523 | def number_of_zones_faulted = current_faults.size()
1524 |
1525 | def new_faults = current_faults.minus(state.faulted_zones)
1526 | def cleared_faults = state.faulted_zones.minus(current_faults)
1527 |
1528 | if (parent.debug) {
1529 | log.trace("Current faulted zones: ${current_faults}")
1530 | log.trace("New faults: ${new_faults}")
1531 | log.trace("Cleared faults: ${cleared_faults}")
1532 | }
1533 |
1534 | // Trigger switches for newly faulted zones.
1535 | for (def i = 0; i < new_faults.size(); i++) {
1536 | if (parent.debug) log.trace("Setting switch ${new_faults[i]}")
1537 | def switch_events = update_zone_switch(new_faults[i], true)
1538 | events = events.plus(switch_events)
1539 | }
1540 |
1541 | // Reset switches for cleared zones.
1542 | for (def i = 0; i < cleared_faults.size(); i++) {
1543 | if (parent.debug) log.trace("Clearing switch ${cleared_faults[i]}")
1544 | def switch_events = update_zone_switch(cleared_faults[i], false)
1545 | events = events.plus(switch_events)
1546 | }
1547 |
1548 | // FIXME: refactor to remove MAX_ZONE_FAULT_TILES
1549 | // Populate AlarmDecoder UI zone tiles
1550 | for (def i = 1; i <= MAX_ZONE_FAULT_TILES; i++) {
1551 | if (number_of_zones_faulted > 0 && i <= number_of_zones_faulted) {
1552 | def d = (device.currentValue("zoneStatus${i}") ?: "0")
1553 | if (d.toInteger() != current_faults[i - 1])
1554 | events << createEvent(
1555 | name: "zoneStatus${i}",
1556 | value: current_faults[i - 1],
1557 | displayed: true)
1558 | } else {
1559 | if (device.currentValue("zoneStatus${i}") != null)
1560 | events << createEvent(
1561 | name: "zoneStatus${i}",
1562 | value: "",
1563 | displayed: true)
1564 | }
1565 | }
1566 |
1567 | state.faulted_zones = current_faults
1568 |
1569 | return events
1570 | }
1571 |
1572 | /**
1573 | * update_zone_switch(zone, faulted)
1574 | *
1575 | * Send a zone update event to the AlarmDecoder service for processing.
1576 | * This sends the raw zone number and it is up to the service to route
1577 | * this event to the correct virtual switch.
1578 | *
1579 | */
1580 | private def update_zone_switch(zone, faulted) {
1581 | def events = []
1582 |
1583 | if (faulted) {
1584 | events << createEvent(
1585 | name: "zone-on",
1586 | value: zone,
1587 | isStateChange: true,
1588 | displayed: false
1589 | )
1590 | } else {
1591 | events << createEvent(
1592 | name: "zone-off",
1593 | value: zone,
1594 | isStateChange: true,
1595 | displayed: false
1596 | )
1597 | }
1598 |
1599 | return events
1600 | }
1601 |
1602 | /**
1603 | * send_keys(keys)
1604 | *
1605 | * Build a GET request HubAction object to call the
1606 | * AlarmDecoder REST api send command.
1607 | */
1608 | def send_keys(String keys) {
1609 | if (parent.debug)
1610 | log.trace("--- send_keys: keys=${keys}")
1611 | else
1612 | log.trace("--- send_keys")
1613 |
1614 | def urn = getDataValue("urn")
1615 | def apikey = _get_api_key()
1616 |
1617 | return hub_http_post(
1618 | urn,
1619 | "/api/v1/alarmdecoder/send?apikey=${apikey}",
1620 | """{ "keys": "${keys}" }"""
1621 | )
1622 | }
1623 |
1624 | /**
1625 | * hub_http_get()
1626 | *
1627 | * Build a GET request HubAction object.
1628 | */
1629 | def hub_http_get(host, path) {
1630 | if (parent.debug)
1631 | log.trace "--- hub_http_get: host=${host}, path=${path}"
1632 |
1633 | def httpRequest = [
1634 | method: "GET",
1635 | path: path,
1636 | headers: [HOST: host]
1637 | ]
1638 |
1639 | return parent.getHubAction(httpRequest, host)
1640 | }
1641 |
1642 | /**
1643 | * hub_http_post()
1644 | *
1645 | * Build a POST request HubAction object.
1646 | */
1647 | def hub_http_post(host, path, body) {
1648 | if (parent.debug)
1649 | log.trace "--- hub_http_post: host=${host}, path=${path} body=${body}"
1650 |
1651 | def httpRequest = [
1652 | method: "POST",
1653 | path: path,
1654 | headers: [HOST: host, "Content-Type": "application/json"],
1655 | body: body
1656 | ]
1657 |
1658 | return parent.getHubAction(httpRequest, host)
1659 | }
1660 |
1661 | /**
1662 | * _get_user_code()
1663 | *
1664 | * Internal routine to return the current user_code setting
1665 | * for the connected AlarmDecoder WEBAPP
1666 | */
1667 | def _get_user_code() {
1668 | return settings.user_code
1669 | }
1670 |
1671 | /**
1672 | * _get_api_key()
1673 | *
1674 | * Internal routine to return the current REST api key setting
1675 | * for the connected AlarmDecoder WEBAPP
1676 | */
1677 | def _get_api_key() {
1678 | return settings.api_key
1679 | }
1680 |
1681 | /**
1682 | * getStateValue(key)
1683 | *
1684 | * helper parent access to state.
1685 | */
1686 | def getStateValue(key) {
1687 | return state[key]
1688 | }
1689 |
--------------------------------------------------------------------------------
/devicetypes/alarmdecoder/alarmdecoder-status-indicator.src/alarmdecoder-status-indicator.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Virtual Status Indicator for alarm panel
3 | *
4 | * Copyright 2016-2019 Nu Tech Software Solutions, Inc.
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not
7 | * use this file except in compliance with the License. You may obtain a copy
8 | * of the License at:
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | * License for the specific language governing permissions and limitations
15 | * under the License.
16 | *
17 | */
18 |
19 | /*
20 | * global support
21 | */
22 | import groovy.transform.Field
23 | @Field APPNAMESPACE = "alarmdecoder"
24 |
25 | metadata {
26 | definition(
27 | name: "AlarmDecoder status indicator",
28 | namespace: APPNAMESPACE,
29 | author: "Nu Tech Software Solutions, Inc.") {
30 | capability "Contact Sensor"
31 | }
32 |
33 | // tile definitions
34 | tiles {
35 | standardTile(
36 | "contact",
37 | "device.contact",
38 | width: 2, height: 2,
39 | canChangeIcon: true) {
40 | state(
41 | "closed",
42 | label: '${name}',
43 | icon: "st.contact.contact.closed",
44 | backgroundColor: "#00a0dc")
45 | state(
46 | "open",
47 | label: '${name}',
48 | icon: "st.contact.contact.open",
49 | backgroundColor: "#e86d13")
50 | }
51 | main "contact"
52 | details "contact"
53 | }
54 |
55 | // preferences
56 | preferences {
57 | input(
58 | name: "invert",
59 | type: "bool",
60 | title: "Invert signal [true,false]",
61 | description: "Invert signal [true,false]." +
62 | " Changes ON/OFF,OPEN/CLOSE,DETECTED/CLEAR",
63 | required: false)
64 | input(
65 | name: "zone",
66 | type: "number",
67 | title: "Zone Number",
68 | description: "Zone # required for zone events.",
69 | required: false)
70 | }
71 | }
72 |
73 | /**
74 | * installed()/updated()
75 | *
76 | * It is not possible for a service to access preferences directly so
77 | * update device data value to allow access from parent
78 | * using getDeviceDataByName getDataValue
79 | * FIXME: diff ^ docs not clear.
80 | *
81 | */
82 | def installed() {
83 | updateDataValue("invert", invert.toString())
84 | updateDataValue("zone", zone.toString())
85 | }
86 |
87 | def updated() {
88 | updateDataValue("invert", invert.toString())
89 | updateDataValue("zone", zone.toString())
90 | }
91 |
--------------------------------------------------------------------------------
/devicetypes/alarmdecoder/alarmdecoder-virtual-carbon-monoxide-detector.src/alarmdecoder-virtual-carbon-monoxide-detector.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Virtual Carbon Monoxide Detector for alarm panel
3 | *
4 | * Copyright 2016-2019 Nu Tech Software Solutions, Inc.
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not
7 | * use this file except in compliance with the License. You may obtain a copy
8 | * of the License at:
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | * License for the specific language governing permissions and limitations
15 | * under the License.
16 | *
17 | */
18 |
19 | /*
20 | * global support
21 | */
22 | import groovy.transform.Field
23 | @Field APPNAMESPACE = "alarmdecoder"
24 |
25 | metadata {
26 | definition(
27 | name: "AlarmDecoder virtual carbon monoxide detector",
28 | namespace: APPNAMESPACE,
29 | author: "Nu Tech Software Solutions, Inc.") {
30 | capability "CarbonMonoxideDetector"
31 | }
32 |
33 | // tile definitions
34 | tiles {
35 | standardTile(
36 | "sensor",
37 | "device.smoke",
38 | width: 2, height: 2,
39 | canChangeIcon: true) {
40 | state(
41 | "clear",
42 | label: '${name}',
43 | icon: "st.alarm.smoke.clear",
44 | backgroundColor: "#79b821")
45 | state(
46 | "detected",
47 | label: '${name}',
48 | icon: "st.alarm.smoke.smoke",
49 | backgroundColor: "#e86d13")
50 | }
51 | main "sensor"
52 | details "sensor"
53 | }
54 |
55 | // preferences
56 | preferences {
57 | input(
58 | name: "invert",
59 | type: "bool",
60 | title: "Invert signal [true,false]",
61 | description: "Invert signal [true,false]." +
62 | " Changes ON/OFF,OPEN/CLOSE,DETECTED/CLEAR",
63 | required: false)
64 | input(
65 | name: "zone",
66 | type: "number",
67 | title: "Zone Number",
68 | description: "Zone # required for zone events.",
69 | required: false)
70 | }
71 | }
72 |
73 | /**
74 | * installed()/updated()
75 | *
76 | * It is not possible for a service to access preferences directly so
77 | * update device data value to allow access from parent
78 | * using getDeviceDataByName getDataValue
79 | * FIXME: diff ^ docs not clear.
80 | *
81 | */
82 | def installed() {
83 | updateDataValue("invert", invert.toString())
84 | updateDataValue("zone", zone.toString())
85 | }
86 |
87 | def updated() {
88 | updateDataValue("invert", invert.toString())
89 | updateDataValue("zone", zone.toString())
90 | }
91 |
--------------------------------------------------------------------------------
/devicetypes/alarmdecoder/alarmdecoder-virtual-contact-sensor.src/alarmdecoder-virtual-contact-sensor.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Virtual Contact Sensor for alarm panel zones
3 | *
4 | * Copyright 2016-2019 Nu Tech Software Solutions, Inc.
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not
7 | * use this file except in compliance with the License. You may obtain a copy
8 | * of the License at:
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | * License for the specific language governing permissions and limitations
15 | * under the License.
16 | *
17 | */
18 |
19 | /*
20 | * global support
21 | */
22 | import groovy.transform.Field
23 | @Field APPNAMESPACE = "alarmdecoder"
24 |
25 | metadata {
26 | definition(
27 | name: "AlarmDecoder virtual contact sensor",
28 | namespace: APPNAMESPACE,
29 | author: "Nu Tech Software Solutions, Inc.") {
30 | capability "Contact Sensor"
31 | }
32 |
33 | // tile definitions
34 | tiles {
35 | standardTile(
36 | "sensor",
37 | "device.contact",
38 | width: 2, height: 2,
39 | canChangeIcon: true) {
40 | state(
41 | "closed",
42 | label: '${name}',
43 | icon: "st.contact.contact.closed",
44 | backgroundColor: "#00a0dc")
45 | state(
46 | "open",
47 | label: '${name}',
48 | icon: "st.contact.contact.open",
49 | backgroundColor: "#e86d13")
50 | }
51 | main "sensor"
52 | details "sensor"
53 | }
54 |
55 | // preferences
56 | preferences {
57 | input(
58 | name: "invert",
59 | type: "bool",
60 | title: "Invert signal [true,false]",
61 | description: "Invert signal [true,false]." +
62 | " Changes ON/OFF,OPEN/CLOSE,DETECTED/CLEAR",
63 | required: false)
64 | input(
65 | name: "zone", type:
66 | "number", title: "Zone Number",
67 | description: "Zone # required for zone events.",
68 | required: false)
69 | }
70 | }
71 |
72 | /**
73 | * installed()/updated()
74 | *
75 | * It is not possible for a service to access preferences directly so
76 | * update device data value to allow access from parent
77 | * using getDeviceDataByName getDataValue
78 | * FIXME: diff ^ docs not clear.
79 | *
80 | */
81 | def installed() {
82 | updateDataValue("invert", invert.toString())
83 | updateDataValue("zone", zone.toString())
84 | }
85 |
86 | def updated() {
87 | updateDataValue("invert", invert.toString())
88 | updateDataValue("zone", zone.toString())
89 | }
90 |
--------------------------------------------------------------------------------
/devicetypes/alarmdecoder/alarmdecoder-virtual-motion-detector.src/alarmdecoder-virtual-motion-detector.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Virtual Motion Detector for alarm panel zones
3 | *
4 | * Copyright 2016-2019 Nu Tech Software Solutions, Inc.
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not
7 | * use this file except in compliance with the License. You may obtain a copy
8 | * of the License at:
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | * License for the specific language governing permissions and limitations
15 | * under the License.
16 | *
17 | */
18 |
19 | /*
20 | * global support
21 | */
22 | import groovy.transform.Field
23 | @Field APPNAMESPACE = "alarmdecoder"
24 |
25 | metadata {
26 | definition(
27 | name: "AlarmDecoder virtual motion detector",
28 | namespace: APPNAMESPACE,
29 | author: "Nu Tech Software Solutions, Inc.") {
30 | capability "Motion Sensor"
31 | }
32 |
33 | // tile definitions
34 | tiles(scale: 2) {
35 | multiAttributeTile(
36 | name: "motion",
37 | type: "generic",
38 | width: 6, height: 4) {
39 | tileAttribute(
40 | "device.motion",
41 | key: "PRIMARY_CONTROL") {
42 | attributeState(
43 | "active",
44 | label: 'motion',
45 | icon: "st.motion.motion.active",
46 | backgroundColor: "#53a7c0")
47 | attributeState(
48 | "inactive",
49 | label: 'no motion',
50 | icon: "st.motion.motion.inactive",
51 | backgroundColor: "#ffffff")
52 | }
53 | }
54 | main "motion"
55 | details "motion"
56 | }
57 |
58 | preferences {
59 | input(
60 | name: "invert",
61 | type: "bool",
62 | title: "Invert signal [true,false]",
63 | description: "Invert signal [true,false]." +
64 | " Changes ON/OFF,OPEN/CLOSE,DETECTED/CLEAR",
65 | required: false)
66 | input(
67 | name: "zone",
68 | type: "number",
69 | title: "Zone Number",
70 | description: "Zone # required for zone events.",
71 | required: false)
72 | }
73 | }
74 |
75 | /**
76 | * installed()/updated()
77 | *
78 | * It is not possible for a service to access preferences directly so
79 | * update device data value to allow access from parent
80 | * using getDeviceDataByName getDataValue
81 | * FIXME: diff ^ docs not clear.
82 | *
83 | */
84 | def installed() {
85 | updateDataValue("invert", invert.toString())
86 | updateDataValue("zone", zone.toString())
87 | }
88 |
89 | def updated() {
90 | updateDataValue("invert", invert.toString())
91 | updateDataValue("zone", zone.toString())
92 | }
93 |
94 | // FIXME: what?
95 | def parse(String description) {
96 | if (description != "updated") {
97 | if (parent.debug)
98 | log.info "parse returned:${description}"
99 | def pair = description.split(":")
100 | createEvent(name: pair[0].trim(), value: pair[1].trim())
101 | }
102 | }
103 |
104 | def active() {
105 | sendEvent(name: "motion", value: "active")
106 | }
107 |
108 | def inactive() {
109 | sendEvent(name: "motion", value: "inactive")
110 | }
111 |
--------------------------------------------------------------------------------
/devicetypes/alarmdecoder/alarmdecoder-virtual-shock-sensor.src/alarmdecoder-virtual-shock-sensor.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Virtual Shock Sensor for alarm panel
3 | *
4 | * Copyright 2016-2019 Nu Tech Software Solutions, Inc.
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not
7 | * use this file except in compliance with the License. You may obtain a copy
8 | * of the License at:
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | * License for the specific language governing permissions and limitations
15 | * under the License.
16 | *
17 | */
18 |
19 | /*
20 | * global support
21 | */
22 | import groovy.transform.Field
23 | @Field APPNAMESPACE = "alarmdecoder"
24 |
25 | metadata {
26 | definition(
27 | name: "AlarmDecoder virtual shock sensor",
28 | namespace: APPNAMESPACE,
29 | author: "Nu Tech Software Solutions, Inc.") {
30 | capability "ShockSensor"
31 | }
32 |
33 | // tile definitions
34 | tiles {
35 | standardTile(
36 | "sensor",
37 | "device.smoke",
38 | width: 2, height: 2,
39 | canChangeIcon: true) {
40 | state(
41 | "clear",
42 | label: '${name}',
43 | icon: "st.alarm.smoke.clear",
44 | backgroundColor: "#79b821")
45 | state(
46 | "detected",
47 | label: '${name}',
48 | icon: "st.alarm.smoke.smoke",
49 | backgroundColor: "#e86d13"
50 | )
51 | }
52 | main "sensor"
53 | details "sensor"
54 | }
55 |
56 | // preferences
57 | preferences {
58 | input(
59 | name: "invert",
60 | type: "bool",
61 | title: "Invert signal [true,false]",
62 | description: "Invert signal [true,false]." +
63 | " Changes ON/OFF,OPEN/CLOSE,DETECTED/CLEAR",
64 | required: false)
65 | input(
66 | name: "zone",
67 | type: "number",
68 | title: "Zone Number",
69 | description: "Zone # required for zone events.",
70 | required: false)
71 | }
72 | }
73 |
74 | /**
75 | * installed()/updated()
76 | *
77 | * It is not possible for a service to access preferences directly so
78 | * update device data value to allow access from parent
79 | * using getDeviceDataByName getDataValue
80 | * FIXME: diff ^ docs not clear.
81 | *
82 | */
83 | def installed() {
84 | updateDataValue("invert", invert.toString())
85 | updateDataValue("zone", zone.toString())
86 | }
87 |
88 | def updated() {
89 | updateDataValue("invert", invert.toString())
90 | updateDataValue("zone", zone.toString())
91 | }
92 |
--------------------------------------------------------------------------------
/devicetypes/alarmdecoder/alarmdecoder-virtual-smoke-alarm.src/alarmdecoder-virtual-smoke-alarm.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Virtual Smoke Alarm for alarm panel
3 | *
4 | * Copyright 2016-2019 Nu Tech Software Solutions, Inc.
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not
7 | * use this file except in compliance with the License. You may obtain a copy
8 | * of the License at:
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | * License for the specific language governing permissions and limitations
15 | * under the License.
16 | *
17 | */
18 |
19 | /*
20 | * global support
21 | */
22 | import groovy.transform.Field
23 | @Field APPNAMESPACE = "alarmdecoder"
24 |
25 | metadata {
26 | definition(
27 | name: "AlarmDecoder virtual smoke alarm",
28 | namespace: APPNAMESPACE,
29 | author: "Nu Tech Software Solutions, Inc.") {
30 | capability "Smoke Detector"
31 | }
32 |
33 | // tile definitions
34 | tiles {
35 | standardTile(
36 | "sensor",
37 | "device.smoke",
38 | width: 2, height: 2,
39 | canChangeIcon: true) {
40 | state(
41 | "clear",
42 | label: '${name}',
43 | icon: "st.alarm.smoke.clear",
44 | backgroundColor: "#79b821")
45 | state(
46 | "detected",
47 | label: '${name}',
48 | icon: "st.alarm.smoke.smoke",
49 | backgroundColor: "#e86d13")
50 | }
51 | main "sensor"
52 | details "sensor"
53 | }
54 |
55 | // preferences
56 | preferences {
57 | input(
58 | name: "invert",
59 | type: "bool",
60 | title: "Invert signal [true,false]",
61 | description: "Invert signal [true,false]." +
62 | " Changes ON/OFF,OPEN/CLOSE,DETECTED/CLEAR",
63 | required: false)
64 | input(
65 | name: "zone",
66 | type: "number",
67 | title: "Zone Number",
68 | description: "Zone # required for zone events.",
69 | required: false)
70 | }
71 | }
72 |
73 | /**
74 | * installed()/updated()
75 | *
76 | * It is not possible for a service to access preferences directly so
77 | * update device data value to allow access from parent
78 | * using getDeviceDataByName getDataValue
79 | * FIXME: diff ^ docs not clear.
80 | *
81 | */
82 | def installed() {
83 | updateDataValue("invert", invert.toString())
84 | updateDataValue("zone", zone.toString())
85 | }
86 |
87 | def updated() {
88 | updateDataValue("invert", invert.toString())
89 | updateDataValue("zone", zone.toString())
90 | }
91 |
--------------------------------------------------------------------------------
/smartapps/alarmdecoder/alarmdecoder-service.src/alarmdecoder-service.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * AlarmDecoder Service Manager
3 | *
4 | * Copyright 2016-2019 Nu Tech Software Solutions, Inc.
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not
7 | * use this file except in compliance with the License. You may obtain a copy
8 | * of the License at:
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | * License for the specific language governing permissions and limitations
15 | * under the License.
16 | *
17 | *
18 | * V1.0.0 - Scott Petersen - Initial design and release 2015/12/10 - 2017/04/20
19 | * V2.0.0 - Sean Mathews - 2018/05/21
20 | * Changed to use UPNP Push API in AD2 web app.
21 | * V2.0.1 - Sean Mathews - 2018/05/21
22 | * Adding CID device management support.
23 | * V2.0.2 - Sean Mathews - 2019/01/02
24 | * Fixed app 20 second max timeout. AddZone is now async, added more zones.
25 | * V2.0.3 - Sean Mathews - 2019/01/11
26 | * Improved/fixed issues with previous app 20 timeout after more testing.
27 | * V2.0.4 - Sean Mathews - 2019/02/19
28 | * Support multiple instances of service by changing unique ID message
29 | * filters by MAC.
30 | * V2.0.5 - Sean Mathews - 2019/03/06
31 | * Add switch to create Disarm button.
32 | * V2.0.6 - Sean Mathews - 2019/03/28
33 | * Compatibility between ST and HT. Still requires some manual code edits but
34 | * it will be minimal.
35 | * V2.0.7 - Sean Mathews - 2019/05/05
36 | * Add RFX virtual device management to track Ademco 5800 wireless or VPLEX
37 | * sensors directly.
38 | * V2.0.8 - Sean Mathews - 2019/05/17
39 | * Added Exit button. New flag from AD2 state to detect exit state.
40 | * Added rebuild devices button.
41 | * V2.0.9 - Sean Mathews - 2019/05/17
42 | * Split some devices from a single combined Momentary to a Momentary and a
43 | * Contact for easy access by other systems.
44 | * V2.1.0 - Sean Mathews - 2019/08/02
45 | * Modified virtual RFX devices to allow inverting signal and capabilities
46 | * detection. Set the RFX device type to AlarmDecoder virtual smoke and it
47 | * will report as a smoke detector. Modified sending of events creating a
48 | * wrapper to invert and adjust to the device type.
49 | * V2.1.1 - Sean Mathews - 2019/09/21
50 | * Add screen to re-link a local AlarmDecoder WEBAPP if the IP/MAC address
51 | * change. Refactoring cleanup localization. Refactoring long lines.
52 | * V2.2.0 - Sean Mathews - 2019/09/20 - 2019/09/30
53 | * A major re-factor and cleanup of the code. Improved UI. Ability to
54 | * reconnect the AlarmDecoder if the IP or MAC change using the App UI.
55 | * Refactor how zones are mapped to devices. Now each virtual device can
56 | * be assigned a zone number in preferences.
57 | *
58 | */
59 |
60 | /**
61 | * global support
62 | */
63 | import groovy.transform.Field
64 | @Field APPNAMESPACE = "alarmdecoder"
65 | @Field SSDPTERM = "urn:schemas-upnp-org:device:AlarmDecoder:1"
66 |
67 | /**
68 | * System Settings
69 | */
70 | @Field debug = false
71 | @Field MAX_VIRTUAL_ZONES = 20
72 | @Field NOCREATEDEV = false
73 | @Field CREATE_DISARM = true
74 |
75 | /**
76 | * Install Notes:
77 | * Modify code in getHubAction below to adjust
78 | * between SmartThings and Hubitat
79 | */
80 |
81 | /**
82 | * Device label name settings
83 | * To run more than once service load this code as a new SmartApp
84 | * and change the idname to something unique. For easy use with Echo
85 | * you can set the '''sname''' to something easy to say such as 'Security'
86 | * then you can say 'Computer - Security Arm Stay On'
87 | */
88 | @Field lname = "AlarmDecoder"
89 | @Field sname = "Security"
90 | @Field guiname = "${lname} UI"
91 | @Field idname = ""
92 |
93 | /**
94 | * CID table
95 | * List of some of the CID #'s and descriptions.
96 | * 000 will trigger a manual input of the CID number.
97 | */
98 | @Field cid_numbers = ["0": "000 - Other / Custom",
99 | "10?": "100-102 - ALL Medical alarms",
100 | "11?": "110-118 - ALL Fire alarms",
101 | "12?": "120-126 - ALL Panic alarms",
102 | "13?": "130-139 - ALL Burglar alarms",
103 | "14?": "140-149 - ALL General alarms",
104 | "1[5-6]?": "150-169 - ALL 24 HOUR AUX alarms",
105 | "154": "154 - Water Leakage",
106 | "158": "158 - High Temp",
107 | "162": "162 - Carbon Monoxide Detected",
108 | "301": "301 - AC Loss",
109 | "3??": "3?? - All System Troubles",
110 | "401": "401 - Arm AWAY OPEN/CLOSE",
111 | "441": "441 - Arm STAY OPEN/CLOSE",
112 | "4[0,4]1": "4[0,4]1 - Arm Stay or Away OPEN/CLOSE"
113 | ]
114 |
115 | /**
116 | * Get the HubAction class specific to
117 | * the HUB type being used.
118 | *
119 | * NOTE:
120 | * Remove comments on code for the HUB type being used.
121 | */
122 | def getHubAction(action, method = null) {
123 |
124 | // SmartThings specific classes here
125 | // Comment out the next 2 lines if we are using Hubitat
126 | if (!method) method = physicalgraph.device.Protocol.LAN
127 | def ha = new physicalgraph.device.HubAction(action, method)
128 |
129 | // Hubitat specific classes here
130 | // Comment out the next line if we are using SmartThings
131 | //if (!method) method = hubitat.device.Protocol.LAN
132 | //def ha = new hubitat.device.HubAction(action, method)
133 |
134 | return ha
135 | }
136 |
137 | /**
138 | * Service definition and preferences
139 | */
140 | definition(
141 | name: "AlarmDecoder service${idname}",
142 | namespace: APPNAMESPACE,
143 | author: "Nu Tech Software Solutions, Inc.",
144 | description: "AlarmDecoder (Service Manager)",
145 | category: "My Apps",
146 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
147 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
148 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
149 | singleInstance: true) {}
150 |
151 | preferences {
152 | page(
153 | name: "page_main",
154 | install: false,
155 | uninstall: false)
156 | page(
157 | name: "page_discover",
158 | content: "page_discover",
159 | install: true,
160 | uninstall: false)
161 | page(
162 | name: "page_remove_all",
163 | content: "page_remove_all",
164 | install: false,
165 | uninstall: false)
166 | page(
167 | name: "page_rebuild_all",
168 | content: "page_rebuild_all",
169 | install: false,
170 | uninstall: false)
171 | page(name: "page_cid_management",
172 | content: "page_cid_management",
173 | install: false,
174 | uninstall: false)
175 | page(name: "page_add_new_cid",
176 | content: "page_add_new_cid",
177 | install: false,
178 | uninstall: false)
179 | page(
180 | name: "page_add_new_cid_confirm",
181 | content: "page_add_new_cid_confirm",
182 | install: false,
183 | uninstall: false)
184 | page(
185 | name: "page_remove_selected_cid",
186 | content: "page_remove_selected_cid",
187 | install: false,
188 | uninstall: false)
189 | page(
190 | name: "page_rfx_management",
191 | content: "page_rfx_management",
192 | install: false,
193 | uninstall: false,
194 | refreshInterval: 5)
195 | page(
196 | name: "page_add_new_rfx",
197 | content: "page_add_new_rfx",
198 | install: false,
199 | uninstall: false)
200 | page(
201 | name: "page_add_new_rfx_confirm",
202 | content: "page_add_new_rfx_confirm",
203 | install: false,
204 | uninstall: false)
205 | page(
206 | name: "page_remove_selected_rfx",
207 | content: "page_remove_selected_rfx",
208 | install: false,
209 | uninstall: false)
210 | page(
211 | name: "page_relink_update",
212 | content: "page_relink_update",
213 | install: false,
214 | uninstall: false)
215 | page(
216 | name: "page_select_device",
217 | content: "page_select_device",
218 | install: false,
219 | uninstall: false,
220 | refreshInterval: 5)
221 | }
222 |
223 | /*
224 | * Localization strings
225 | */
226 | def szt(String name, Object... args) {
227 | def en_strings = [
228 |
229 | // misc or used multiple places
230 | "home": "home",
231 | "home_screen": "Press the < arrow above two times to return home.",
232 | "no_save_note": "Do not use the \"Save\" buttons on this page.",
233 | "save_note": "Press \"Save\" buttons on this page to install the service devices.",
234 | "input_selected_devices_title": "Select device (%s found).",
235 | "section_monitor_integration": "Monitor integration",
236 | "section_zone_sensor_settings": "Zone Sensors",
237 | "section_mon_integration": "Monitor Integration",
238 | "tap_here": "[ * TAP HERE * ]",
239 | "press_back_note": "Press the < arrow above to return to the previous page.",
240 |
241 | // Main Page
242 | "page_main_title": "Setup And Management",
243 | "page_main_device_found": "${lname} service found.\nSelect from management options below.",
244 |
245 | // Discover/Install
246 | "page_discover_title": "Install Service",
247 | "page_discover_desc": "Tap to discover and install your ${lname} Appliance.",
248 | "page_discover_section_selected_device": "Selected device info: %s",
249 |
250 | // Service Settings
251 | "monIntegrationSHM": "Integrate with Smart Home Monitor?",
252 | "monIntegrationHSM": "Integrate with Home Security Monitor?",
253 | "monChangeStatusSHM": "Automatically change Smart Home Monitor status when armed or disarmed?",
254 | "monChangeStatusHSM": "Automatically change Home Security Monitor status when armed or disarmed?",
255 | "defaultSensorToClosed": "Default zone sensors to closed?",
256 |
257 | // CID Management
258 | "page_cid_management_title": "Contact ID Device Management",
259 | "page_cid_management_desc": "Tap to manage virtual devices.",
260 | "input_cid_devices_title": "Remove installed CID virutal devices.",
261 | "input_cid_devices_desc": "Tap to select.",
262 |
263 | //// Add new CID
264 | "page_add_new_cid_title": "Add New Contact ID Virtual Switch",
265 | "page_add_new_cid_desc": "Tap to add new CID switch",
266 | "section_build_cid": "CODE mask: %s",
267 | "input_cid_number_title": "Select the CID number for this device",
268 | "input_cid_number_desc": "Tap to select",
269 | "input_cid_number_raw_title": "Enter raw Contact ID CODE or simple regex pattern",
270 | "section_cid_value": "USER# or ZONE# mask : %s",
271 | "input_cid_value_title": "Zero padded 3 digit User# or Zone# or simple regex pattern ex. '001' or '???'",
272 | "section_cid_partition": "Partition mask: %s",
273 | "input_cid_partition_title": "Enter the partition. Use 0 for system and ? for any.",
274 | "section_cid_name": "Device Name",
275 | "input_cid_name_title": "Enter the new device name or blank for auto",
276 | "section_cid_label": "Device Label",
277 | "input_cid_label_title": "Enter the new device label or blank for auto",
278 |
279 | ////// Add new confirm
280 | "page_add_new_cid_confirm_title": "Add new CID switch : %s",
281 | "add_new_cid_confirm_info": "Attempted to add new CID device. This may fail if the device is in use. If it fails review the log. This screen is probably no longer valid so you may get errors on the app until you exit these pages. Press back to continue.",
282 | "href_add_new_cid_confirm_desc": "Tap to confirm and add",
283 |
284 | ////// Remove CID
285 | "page_remove_selected_cid_title": "Remove selected virtual CID devices",
286 | "page_remove_selected_cid_desc": "Tap to remove selected virtual CID device",
287 | "page_remove_selected_cid_info": "Attempted to remove selected devices. This may fail if the device is in use. If it fails review the log and manually remove all devices and remove the service from the location. Press back to continue.",
288 |
289 | // RFX Management
290 | "page_rfx_management_title": "RFX Device Management",
291 | "page_rfx_management_desc": "Tap to manage virtual devices.",
292 | "input_rfx_devices_title": "Selected RFX devices to remove.",
293 | "input_rfx_devices_desc": "Tap to select.",
294 | "page_remove_selected_rfx_title": "Remove selected RFX devices.",
295 | "page_add_new_rfx_title": "Add New RFX Virtual Device",
296 | "page_add_new_rfx_desc": "To add new RFX virtual device\n%s",
297 | "page_add_new_rfx_confirm": "Tap to confirm and add.",
298 |
299 | "page_remove_selected_rfx": "Remove Selected Virtual Device.",
300 | "section_build_rfx": "Build Device Name :",
301 | "input_rfx_name": "Enter the new device name or blank for auto.",
302 | "input_rfx_label": "Enter the new device label or blank for auto.",
303 | "input_rfx_sn": "Enter Serial # or simple REGEX pattern.",
304 | "input_rfx_supv": "Supervision bit: Enter 1 to watch or ? to ignore.",
305 | "input_rfx_bat": "Battery: Enter 1 to monitor or ? to ignore.",
306 | "input_rfx_loop0": "Loop 0: Enter 1 to monitor or ? to ignore.",
307 | "input_rfx_loop1": "Loop 1: Enter 1 to monitor or ? to ignore.",
308 | "input_rfx_loop2": "Loop 2: Enter 1 to monitor or ? to ignore.",
309 | "input_rfx_loop3": "Loop 3: Enter 1 to monitor or ? to ignore.",
310 |
311 | // Select Device discovery
312 | "page_select_device_title": "Select ${lname} Appliance",
313 | "page_select_device_desc": "To select the local ${lname} Appliance this service will connect to. Be sure the WEBAPP UPNP notification service is enabled.",
314 |
315 | // Update AD2 IP/MAC relink
316 | "info_confirm_relink_update": "To update the ${lname} service to link to the selected device at (%s).",
317 | "page_relink_update_title": "Update Service Settings",
318 | "page_relink_update_desc": "This page updates settings or re-link the ${lname} Appliance if the IP or MAC address change.\nTap to select.",
319 | "page_relink_section_active_device": "Linked device info: %s",
320 | "page_relink_section_selected_device": "Selected device info: %s",
321 | "info_relink_update_done": "Attempted to re-link the selected devices. This may fail. If it fails review the log and provide feedback.",
322 |
323 |
324 | // Rebuild All
325 | "page_rebuild_all_title": "Rebuild Virtual Devices",
326 | "page_rebuild_all_desc": "This page will find and repair missing virtual devices.\nTap to select.",
327 | "confirm_rebuild_all": "Tap to confirm and rebuild all.",
328 | "info_rebuild_all_done": "Rebuild done. Press back arrow to return to home screen.",
329 | "info_rebuild_all_confirm": "This will attempt to rebuild all child devices. Monitor the logs for any errors. Press back to return to the main page.",
330 |
331 | // Remove All
332 | "page_remove_all_title": "Uninstall ${lname} Service",
333 | "page_remove_all_desc": "This page is for removing and uninstalling the ${lname} service.\nTap to select.",
334 | "remove_all_href_confirm": "[ * TAP HERE * ]",
335 | "info_remove_all_done": "Removed all child devices. Press back to return to the main page.",
336 | "info_remove_all_confirm": "This will attempt to remove all child devices. This may fail if the device is in use. If it fails review the log and manually remove the usage.\nTap to confirm.",
337 |
338 |
339 | // End
340 | "": ""
341 | ]
342 | if (args) {
343 | try {
344 | return String.format(en_strings[name], args)
345 | } catch (Exception ex) {
346 | log.error("***SZT***:${name}")
347 | return "**SZT***:err:${name}"
348 | }
349 | } else {
350 | return en_strings[name]
351 | }
352 | }
353 |
354 | /**
355 | * Allow remote device to force the HUB to request an
356 | * update from the AlarmDecoder.
357 | *
358 | * Just a few steps :( but it works.
359 | * AD2 -> ST-CLOUD -> ST-HUB -> AD2 -> ST-HUB -> ST-CLOUD
360 | */
361 | mappings {
362 | path("/update") {
363 | action: [
364 | GET: "webserviceUpdate"
365 | ]
366 | }
367 | }
368 |
369 |
370 | /*** Pages/UI ***/
371 |
372 | /**
373 | * Misc helper sections
374 | */
375 | def section_save_note() {
376 | section(szt("save_note")) {}
377 | }
378 |
379 | def section_no_save_note() {
380 | section(szt("no_save_note")) {}
381 | }
382 |
383 | def section_home() {
384 | section(szt("home")) {
385 | href(
386 | name: "href_home",
387 | title: szt("tap_here"),
388 | required: false,
389 | description: szt("home_screen"),
390 | page: "page_main"
391 | )
392 | }
393 | }
394 |
395 | def section_back_note() {
396 | section(szt("press_back_note")) {}
397 | }
398 |
399 | /**
400 | * The main service page
401 | */
402 | def page_main() {
403 |
404 | // make sure we are listening to all network subscriptions
405 | initSubscriptions()
406 |
407 | // send out a UPNP broadcast discovery
408 | discover_alarmdecoder()
409 |
410 | // see if we are already installed
411 | def foundMsg = ""
412 | def children = getChildDevices()
413 | if (children) foundMsg = szt("page_main_device_found")
414 |
415 | dynamicPage(name: "page_main", title: szt("page_main_title")) {
416 | if (!children) {
417 | // Not installed show discovery page to complete the install.
418 | section("") {
419 | href(
420 | name: "href_discover",
421 | title: szt("page_discover_title"),
422 | required: false,
423 | description: szt("page_discover_desc"),
424 | page: "page_discover"
425 | )
426 | }
427 | } else {
428 | section(foundMsg) {
429 | href(
430 | name: "href_cid_management",
431 | title: szt("page_cid_management_title"),
432 | required: false,
433 | description: szt("page_cid_management_desc"),
434 | page: "page_cid_management"
435 | )
436 | }
437 | section("") {
438 | href(
439 | name: "href_rfx_management",
440 | title: szt("page_rfx_management_title"),
441 | required: false,
442 | description: szt("page_rfx_management_desc"),
443 | page: "page_rfx_management"
444 | )
445 | }
446 | section("") {
447 | href(
448 | name: "href_relink_update",
449 | title: szt("page_relink_update_title"),
450 | required: false,
451 | description: szt("page_relink_update_desc"),
452 | page: "page_relink_update"
453 | )
454 | }
455 | section("") {
456 | href(
457 | name: "href_rebuild_all",
458 | title: szt("page_rebuild_all_title"),
459 | required: false,
460 | description: szt("page_rebuild_all_desc"),
461 | page: "page_rebuild_all"
462 | )
463 | }
464 | section("") {
465 | href(
466 | name: "href_remove_all",
467 | title: szt("page_remove_all_title"),
468 | required: false,
469 | description: szt("page_remove_all_desc"),
470 | page: "page_remove_all"
471 | )
472 | }
473 | }
474 | }
475 | }
476 |
477 | /**
478 | * Page page_cid_management generator.
479 | */
480 | def page_cid_management() {
481 | // TODO: Find a way to clear our current values on loading page
482 | return\
483 | dynamicPage(
484 | name: "page_cid_management",
485 | title: szt("page_cid_management_title")
486 | ) {
487 | def found_devices = []
488 | getAllChildDevices().each {
489 | device->
490 | if (device.deviceNetworkId.contains(":CID-")) {
491 | found_devices << getDeviceNamePart(device)
492 | }
493 | }
494 | section_no_save_note()
495 | section {
496 | if (found_devices.size()) {
497 | input(
498 | name: "input_cid_devices",
499 | type: "enum",
500 | required: false,
501 | multiple: true,
502 | options: found_devices,
503 | title: szt("input_cid_devices_title"),
504 | description: szt("input_cid_devices_desc"),
505 | submitOnChange: true
506 | )
507 | if (input_cid_devices) {
508 | href(
509 | name: "href_remove_selected_cid",
510 | required: false,
511 | page: "page_remove_selected_cid",
512 | title: szt("page_remove_selected_cid_title"),
513 | description: szt("page_remove_selected_cid_desc")
514 | )
515 | }
516 | }
517 | }
518 | section {
519 | href(
520 | name: "href_add_new_cid",
521 | required: false,
522 | page: "page_add_new_cid",
523 | title: szt("page_add_new_cid_title"),
524 | description: szt("page_add_new_cid_desc")
525 | )
526 | }
527 | section_back_note()
528 | }
529 | }
530 |
531 | /**
532 | * Page page_remove_selected_cid generator
533 | */
534 | def page_remove_selected_cid() {
535 | def errors = []
536 | getAllChildDevices().each {
537 | device->
538 | if (device.deviceNetworkId.contains(":CID-")) {
539 | // Only remove the one that matches our list
540 | def device_name = getDeviceNamePart(device)
541 | def d = input_cid_devices.find {
542 | it == device_name
543 | }
544 | if (d) {
545 | log.trace("removing CID device ${device.deviceNetworkId}")
546 | try {
547 | deleteChildDevice(device.deviceNetworkId)
548 | input_cid_devices.remove(device_name)
549 | errors << "Success removing " + device_name
550 | } catch (e) {
551 | log.error("There was an error (${e}) when trying " +
552 | "to delete the child device")
553 | errors << "Error removing " + device_name
554 | }
555 | }
556 | }
557 | }
558 |
559 | return\
560 | dynamicPage(
561 | name: "page_remove_selected_cid",
562 | title: szt("page_remove_selected_cid_title")
563 | ) {
564 | section_no_save_note()
565 | section {
566 | paragraph szt("page_remove_selected_cid_info")
567 | errors.each {
568 | error->
569 | paragraph(error)
570 | }
571 | }
572 | section_back_note()
573 | }
574 | }
575 |
576 |
577 | /**
578 | * Page page_add_new_cid generator
579 | */
580 | def page_add_new_cid() {
581 |
582 | return\
583 | dynamicPage(
584 | name: "page_add_new_cid",
585 | title: szt("page_add_new_cid_title")
586 | ) {
587 | section_no_save_note()
588 | // show pre defined CID number templates to select from
589 | section(szt("section_build_cid", buildcid())) {
590 | input(
591 | name: "input_cid_number",
592 | type: "enum",
593 | required: true,
594 | multiple: false,
595 | options: cid_numbers,
596 | title: szt("input_cid_number_title"),
597 | description: szt("input_cid_number_desc"),
598 | submitOnChange: true
599 | )
600 | }
601 | // if a CID entry is selected then check the value if it is "0"
602 | // to show raw input section
603 | if (input_cid_number) {
604 | if (input_cid_number == "0") {
605 | section {
606 | input(
607 | name: "input_cid_number_raw",
608 | type: "text",
609 | required: true,
610 | title: szt("input_cid_number_raw_title"),
611 | defaultValue: 110,
612 | submitOnChange: true
613 | )
614 | }
615 | }
616 | section(szt("section_cid_value", buildcidvalue())) {
617 | input(
618 | name: "input_cid_value",
619 | type: "text",
620 | required: true,
621 | title: szt("input_cid_value_title"),
622 | defaultValue: "???",
623 | submitOnChange: true
624 | )
625 | }
626 | section(szt("section_cid_partition", input_cid_partition)) {
627 | input(
628 | name: "input_cid_partition",
629 | type: "enum",
630 | required: true,
631 | defaultValue: 1,
632 | options: ['?', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
633 | submitOnChange: true,
634 | title: szt("input_cid_partition_title")
635 | )
636 | }
637 | section(szt("section_cid_name")) {
638 | input(
639 | name: "input_cid_name",
640 | type: "text",
641 | required: false,
642 | defaultValue: '',
643 | submitOnChange: true,
644 | title: szt("input_cid_name_title")
645 | )
646 | }
647 | section(szt("section_cid_label")) {
648 | input(
649 | name: "input_cid_label",
650 | type: "text",
651 | required: false,
652 | defaultValue: '',
653 | submitOnChange: true,
654 | title: szt("input_cid_label_title")
655 | )
656 | }
657 | // If input_cid_number or input_cid_number_raw have a value
658 | if ((input_cid_number && (input_cid_number != "0")) ||
659 | (input_cid_number_raw)) {
660 | section("") {
661 | href(
662 | name: "href_add_new_cid_confirm",
663 | required: false,
664 | page: "page_add_new_cid_confirm",
665 | title: szt("page_add_new_cid_confirm_title",
666 | buildcidlabel() + "(" + buildcidnetworkid() + ")"),
667 | description: szt("href_add_new_cid_confirm_desc")
668 | )
669 | }
670 | }
671 | section_back_note()
672 | }
673 | }
674 | }
675 |
676 | /**
677 | * page_add_new_cid helpers
678 | */
679 | def buildcid() {
680 | def cidnum = ""
681 | if (input_cid_number == "0") {
682 | cidnum = input_cid_number_raw
683 | } else {
684 | cidnum = input_cid_number
685 | }
686 | return cidnum
687 | }
688 |
689 | def buildcidname() {
690 | if (input_cid_name) {
691 | return "CID-" + input_cid_name
692 | } else {
693 | return buildcidnetworkid()
694 | }
695 | }
696 |
697 | def buildcidlabel() {
698 | if (input_cid_label) {
699 | return "CID-" + input_cid_label
700 | } else {
701 | return buildcidnetworkid()
702 | }
703 | }
704 |
705 | def buildcidnetworkid() {
706 | // get the CID value
707 | def newcid = buildcid()
708 |
709 | def cv = buildcidvalue()
710 | def pt = input_cid_partition
711 | return "CID-${newcid}-${pt}-${cv}"
712 | }
713 |
714 | def buildcidvalue() {
715 | def cidval = input_cid_value
716 | return cidval
717 | }
718 |
719 | /**
720 | * Page page_add_new_cid_confirm generator.
721 | */
722 | def page_add_new_cid_confirm() {
723 | def errors = []
724 | // get the CID value
725 | def newcidlabel = buildcidlabel()
726 | def newcidname = buildcidname()
727 | def newcidnetworkid = buildcidnetworkid()
728 | def cv = input_cid_value
729 | def pt = input_cid_partition
730 |
731 | // Add virtual CID switch if it does not exist.
732 | def d = getChildDevice("${getDeviceKey()}:${newcidlabel}")
733 | if (!d) {
734 | def nd = \
735 | addChildDevice(
736 | APPNAMESPACE,
737 | "AlarmDecoder action button indicator",
738 | "${getDeviceKey()}:${newcidnetworkid}",
739 | state.hubId,
740 | [
741 | name: "${getDeviceKey()}:${newcidname}",
742 | label: "${sname} ${newcidlabel}",
743 | completedSetup: true
744 | ]
745 | )
746 | nd.sendEvent(
747 | name: "switch",
748 | value: "off",
749 | isStateChange: true,
750 | displayed: false
751 | )
752 | errors << "Success adding ${newcidlabel}"
753 | } else {
754 | errors << "Error adding ${newcidlabel}: Exists"
755 | }
756 |
757 | return\
758 | dynamicPage(
759 | name: "page_add_new_cid_confirm",
760 | title: buildcidlabel()
761 | ) {
762 | section_no_save_note()
763 | section(szt("add_new_cid_confirm_info")) {
764 | errors.each {
765 | error->
766 | paragraph(error)
767 | }
768 | }
769 | section_back_note()
770 | }
771 | }
772 |
773 | /**
774 | * Page page_rfx_management generator.
775 | */
776 | def page_rfx_management() {
777 | return\
778 | dynamicPage(
779 | name: "page_rfx_management",
780 | title: szt("page_rfx_management_title")
781 | ) {
782 | def found_devices = []
783 | getAllChildDevices().each {
784 | device->
785 | if (device.deviceNetworkId.contains(":RFX-")) {
786 | found_devices << \
787 | getDeviceNamePart(device)
788 | }
789 | }
790 | section_no_save_note()
791 | section("") {
792 | if (found_devices.size()) {
793 | input(
794 | name: "input_rfx_devices",
795 | type: "enum",
796 | required: false,
797 | multiple: true,
798 | options: found_devices,
799 | title: szt("input_rfx_devices_title"),
800 | description: szt("input_rfx_devices_desc"),
801 | submitOnChange: true
802 | )
803 | if (input_rfx_devices) {
804 | href(
805 | name: "href_remove_selected_rfx",
806 | required: false,
807 | page: "page_remove_selected_rfx",
808 | title: szt("page_remove_selected_rfx_title"),
809 | description: szt("href_remove_selected_rfx_desc")
810 | )
811 | }
812 | }
813 | }
814 | section("") {
815 | href(
816 | name: "href_add_new_rfx",
817 | required: false,
818 | page: "page_add_new_rfx",
819 | title: szt("tap_here"),
820 | description: szt("page_add_new_rfx_desc", "")
821 | )
822 | }
823 | section_back_note()
824 | }
825 | }
826 |
827 | /**
828 | * Page page_remove_selected_rfx generator
829 | */
830 | def page_remove_selected_rfx() {
831 | def errors = []
832 | getAllChildDevices().each {
833 | device->
834 | if (device.deviceNetworkId.contains(":RFX-")) {
835 | // Only remove the one that matches our list
836 | def device_name = getDeviceNamePart(device)
837 | def d = input_rfx_devices.find {
838 | it == device_name
839 | }
840 | if (d) {
841 | log.trace("removing RFX device ${device.deviceNetworkId}")
842 | try {
843 | deleteChildDevice(device.deviceNetworkId)
844 | input_rfx_devices.remove(device_name)
845 | errors << "Success removing " + device_name
846 | } catch (e) {
847 | log.error "There was an error (${e}) when trying" +
848 | " to delete the child device"
849 | errors << "Error removing " + device_name
850 | }
851 | }
852 | }
853 | }
854 |
855 | return dynamicPage(
856 | name: "page_remove_selected_rfx",
857 | title: szt("page_remove_selected_rfx_title")
858 | ) {
859 | section_no_save_note()
860 | section(szt("info_page_remove_selected_rfx")) {
861 | errors.each {
862 | error->
863 | paragraph(error)
864 | }
865 | }
866 | section_back_note()
867 | }
868 | }
869 |
870 | /**
871 | * Page page_add_new_rfx generator
872 | */
873 | def page_add_new_rfx() {
874 |
875 | return\
876 | dynamicPage(
877 | name: "page_add_new_rfx",
878 | title: szt("page_add_new_rfx_title")
879 | ) {
880 | section_no_save_note()
881 | section(szt("section_build_rfx")) {
882 | paragraph szt("section_rfx_names")
883 | input(
884 | name: "input_rfx_label",
885 | type: "text",
886 | required: false,
887 | defaultValue: '',
888 | submitOnChange: true,
889 | title: szt("input_rfx_label")
890 | )
891 | input(
892 | name: "input_rfx_name",
893 | type: "text",
894 | required: false,
895 | defaultValue: '',
896 | submitOnChange: true,
897 | title: szt("input_rfx_name")
898 | )
899 | }
900 | section {
901 | input(
902 | name: "input_rfx_sn",
903 | type: "text",
904 | required: true,
905 | defaultValue: '000000',
906 | submitOnChange: true,
907 | title: szt("input_rfx_sn")
908 | )
909 | input(
910 | name: "input_rfx_bat",
911 | type: "text",
912 | required: true,
913 | defaultValue: '?',
914 | submitOnChange: true,
915 | title: szt("input_rfx_bat")
916 | )
917 | input(
918 | name: "input_rfx_supv",
919 | type: "text",
920 | required: true,
921 | defaultValue: '?',
922 | submitOnChange: true,
923 | title: szt("input_rfx_supv")
924 | )
925 | input(
926 | name: "input_rfx_loop0",
927 | type: "text",
928 | required: true,
929 | defaultValue: '1',
930 | submitOnChange: true,
931 | title: szt("input_rfx_loop0")
932 | )
933 | input(
934 | name: "input_rfx_loop1",
935 | type: "text",
936 | required: true,
937 | defaultValue: '?',
938 | submitOnChange: true,
939 | title: szt("input_rfx_loop1")
940 | )
941 | input(
942 | name: "input_rfx_loop2",
943 | type: "text",
944 | required: true,
945 | defaultValue: '?',
946 | submitOnChange: true,
947 | title: szt("input_rfx_loop2")
948 | )
949 | input(
950 | name: "input_rfx_loop3",
951 | type: "text",
952 | required: true,
953 | defaultValue: '?',
954 | submitOnChange: true,
955 | title: szt("input_rfx_loop3")
956 | )
957 | }
958 | section {
959 | href(
960 | name: "href_add_new_rfx_confirm",
961 | required: false,
962 | page: "page_add_new_rfx_confirm",
963 | title: szt("tap_here"),
964 | description: szt("page_add_new_rfx_desc",
965 | "${buildrfxlabel()} (${buildrfxnetworkid()})")
966 | )
967 | }
968 | section_back_note()
969 | }
970 | }
971 |
972 | /**
973 | * page_add_new_rfx helpers
974 | */
975 | def buildrfx() {
976 | return input_rfx_sn
977 | }
978 |
979 | def buildrfxname() {
980 | if (input_rfx_name) {
981 | return "RFX-" + input_rfx_name
982 | } else {
983 | return buildrfxnetworkid()
984 | }
985 | }
986 |
987 | def buildrfxlabel() {
988 | if (input_rfx_label) {
989 | return "RFX-" + input_rfx_label
990 | } else {
991 | return buildrfxnetworkid()
992 | }
993 | }
994 |
995 | def buildrfxnetworkid() {
996 | // get the RFX value
997 | def newrfx = buildrfx()
998 |
999 | def cv = buildrfxvalue()
1000 | return "RFX-${newrfx}-${cv}"
1001 | }
1002 |
1003 | def buildrfxvalue() {
1004 | def rfxval = "${input_rfx_bat}-${input_rfx_supv}-${input_rfx_loop0}-" +
1005 | "${input_rfx_loop1}-${input_rfx_loop2}-${input_rfx_loop3}"
1006 | return rfxval
1007 | }
1008 |
1009 | /**
1010 | * Page page_add_new_rfx_confirm generator.
1011 | */
1012 | def page_add_new_rfx_confirm() {
1013 | def errors = []
1014 | // get the RFX value
1015 | def newrfxlabel = buildrfxlabel()
1016 | def newrfxname = buildrfxname()
1017 | def newrfxnetworkid = buildrfxnetworkid()
1018 | def cv = input_rfx_value
1019 |
1020 | // Add virtual RFX switch if it does not exist.
1021 | def d = getChildDevice("${getDeviceKey()}:${newrfxnetworkid}")
1022 | if (!d) {
1023 | def nd = addChildDevice(
1024 | APPNAMESPACE,
1025 | "AlarmDecoder action button indicator",
1026 | "${getDeviceKey()}:${newrfxnetworkid}",
1027 | state.hubId,
1028 | [
1029 | name: "${getDeviceKey()}:${newrfxname}",
1030 | label: "${sname} ${newrfxlabel}",
1031 | completedSetup: true
1032 | ]
1033 | )
1034 | nd.sendEvent(
1035 | name: "switch",
1036 | value: "off",
1037 | isStateChange: true,
1038 | displayed: false
1039 | )
1040 | errors << "Success adding ${newrfxlabel}"
1041 | } else {
1042 | errors << "Error adding ${newrfxlabel}: Exists"
1043 | }
1044 |
1045 | return\
1046 | dynamicPage(
1047 | name: "page_add_new_rfx_confirm",
1048 | title: buildrfxlabel()
1049 | ) {
1050 | section_no_save_note()
1051 | section("") {
1052 | paragraph szt("info_add_new_rfx_confirm")
1053 | errors.each {
1054 | error->
1055 | paragraph(error)
1056 | }
1057 | }
1058 | section_back_note()
1059 | }
1060 | }
1061 |
1062 | /**
1063 | * Page page_rebuild_all generator.
1064 | */
1065 | def page_rebuild_all(params) {
1066 | def message = ""
1067 |
1068 | return\
1069 | dynamicPage(
1070 | name: "page_rebuild_all",
1071 | title: szt("page_rebuild_all_title")
1072 | ) {
1073 | section_no_save_note()
1074 | if (params?.confirm) {
1075 | // Call rebuild device function here
1076 | addExistingDevices()
1077 | message = szt("info_rebuild_all_done")
1078 | } else {
1079 | section("") {
1080 | href(
1081 | name: "href_confirm_rebuild_all_devices",
1082 | title: szt("confirm_rebuild_all"),
1083 | description: szt("href_rebuild_devices"),
1084 | required: false,
1085 | page: "page_rebuild_all",
1086 | params: [confirm: true]
1087 | )
1088 | }
1089 | message = szt("info_rebuild_all_confirm")
1090 | }
1091 | section("") {
1092 | paragraph message
1093 | }
1094 | section_back_note()
1095 | }
1096 | }
1097 |
1098 | /**
1099 | * Page page_remove_all generator.
1100 | */
1101 | def page_remove_all(params) {
1102 | def message = ""
1103 |
1104 | return\
1105 | dynamicPage(
1106 | name: "page_remove_all",
1107 | title: szt("page_remove_all_title")
1108 | ) {
1109 | section_no_save_note()
1110 | if (params?.confirm) {
1111 | uninstalled()
1112 | message = szt("info_remove_all_done")
1113 | } else {
1114 | section("") {
1115 | href(
1116 | name: "href_confirm_remove_all_devices",
1117 | title: szt("remove_all_href_confirm"),
1118 | description: szt("info_remove_all_confirm"),
1119 | required: false,
1120 | page: "page_remove_all",
1121 | params: [confirm: true]
1122 | )
1123 | }
1124 | }
1125 | section("") {
1126 | paragraph message
1127 | }
1128 | section_back_note()
1129 | }
1130 | }
1131 |
1132 | /**
1133 | * Page page_select_device generator
1134 | * reloaded every N seconds to refresh list
1135 | */
1136 | def page_select_device() {
1137 | // send out UPNP discovery messages and watch for responses
1138 | discover_alarmdecoder()
1139 |
1140 | // build list of currently known AlarmDecoder parent devices
1141 | def found_devices = [: ]
1142 | def options = getDevices().each { k, v ->
1143 | if (debug) log.debug "page_select: ${v}"
1144 | def ip = convertHexToIP(v.ip)
1145 | found_devices["${v.ip}:${v.port}"] = "AlarmDecoder @ ${ip}"
1146 | }
1147 |
1148 | // How many do we have?
1149 | def numFound = found_devices.size() ?: 0
1150 |
1151 | return\
1152 | dynamicPage(
1153 | name: "page_select_device",
1154 | title: szt("page_select_device_title")
1155 | ) {
1156 | section_no_save_note()
1157 | section("Discovered devices: (scanning)") {
1158 | input(
1159 | name: "input_selected_devices",
1160 | type: "enum",
1161 | required: false,
1162 | title: szt("input_selected_devices_title", numFound),
1163 | multiple: false,
1164 | submitOnChange: true,
1165 | options: found_devices
1166 | )
1167 | }
1168 | section_back_note()
1169 | }
1170 | }
1171 |
1172 | /**
1173 | * Page page_discover generator.
1174 | */
1175 | def page_discover() {
1176 |
1177 | // send out UPNP discovery messages and watch for responses
1178 | discover_alarmdecoder()
1179 |
1180 | // build list of currently known AlarmDecoder parent devices
1181 | def found_devices = [: ]
1182 | log.debug "devices ${getDevices()}"
1183 | def options = getDevices().each { k, v ->
1184 | if (debug) log.debug "page_discover: ${v}"
1185 | def ip = convertHexToIP(v.ip)
1186 | found_devices["${v.ip}:${v.port}"] = "AlarmDecoder @ ${ip}"
1187 | }
1188 |
1189 | // How many do we have?
1190 | def numFound = found_devices.size() ?: 0
1191 |
1192 | // Load strings for the correct platform
1193 | def monitor_suffix = ""
1194 | if (isSmartThings())
1195 | monitor_suffix = "SHM"
1196 | else if (isHubitat())
1197 | monitor_suffix = "HSM"
1198 |
1199 | return\
1200 | dynamicPage(
1201 | name: "page_discover",
1202 | title: szt("page_discover_title")
1203 | ) {
1204 | def section_select_device_heading = ""
1205 |
1206 | if (input_selected_devices) {
1207 | // Find the discovered device with a matching
1208 | // dni XXXXXXXX:XXXX in input_selected_devices
1209 | def d = \
1210 | getDevices().find { k, v -> "${v.ip}:${v.port}" ==
1211 | "${input_selected_devices}"
1212 | }
1213 |
1214 | def dni = getDeviceKey(d?.value?.ip, d?.value?.port)
1215 | def urn = getHostAddressFromDNI(input_selected_devices)
1216 | def ssdpPath = d?.value?.ssdpPath
1217 | def mac = d ?.value?.mac
1218 | def uuid = d?.value?.ssdpUSN
1219 |
1220 | section_select_device_heading =
1221 | szt("page_discover_section_selected_device",
1222 | "\ndni: ${dni}\nurn: ${urn}\nssdpPath: ${ssdpPath}\n" +
1223 | "mac: ${mac}\nusn: ${uuid}")
1224 | section_save_note()
1225 | }
1226 | section(section_select_device_heading) {
1227 | href(
1228 | name: "href_confirm_discover_update",
1229 | title: szt("tap_here"),
1230 | description: szt("page_select_device_desc"),
1231 | required: false,
1232 | page: "page_select_device"
1233 | )
1234 | }
1235 | section(szt("section_mon_integration")) {
1236 | input(
1237 | name: "monIntegration",
1238 | type: "bool",
1239 | defaultValue: true,
1240 | title: szt("monIntegration${monitor_suffix}")
1241 | )
1242 | input(
1243 | name: "monChangeStatus",
1244 | type: "bool",
1245 | defaultValue: true,
1246 | title: szt("monChangeStatus${monitor_suffix}")
1247 | )
1248 | }
1249 | section(szt("section_zone_sensor_settings")) {
1250 | input(
1251 | name: "defaultSensorToClosed",
1252 | type: "bool",
1253 | defaultValue: true,
1254 | title: szt("defaultSensorToClosed")
1255 | )
1256 | }
1257 |
1258 | section_back_note()
1259 | }
1260 | }
1261 |
1262 | /**
1263 | * Page page_relink_update generator.
1264 | */
1265 | def page_relink_update(params) {
1266 | def message = ""
1267 | def errors = []
1268 |
1269 | // Load strings for the correct platform
1270 | def monitor_suffix = ""
1271 | if (isSmartThings())
1272 | monitor_suffix = "SHM"
1273 | else if (isHubitat())
1274 | monitor_suffix = "HSM"
1275 |
1276 | return\
1277 | dynamicPage(
1278 | name: "page_relink_update",
1279 | title: szt("page_relink_update_title")
1280 | ) {
1281 | section_no_save_note()
1282 |
1283 | if (params?.confirm) {
1284 | message = szt("info_relink_update_done")
1285 | // re-init subs just in case they were lost in the cloud.
1286 | initSubscriptions()
1287 |
1288 | // current device key before we change it
1289 | def dkey = getDeviceKey()
1290 |
1291 | // new data
1292 | def dni = params?.dni
1293 | def urn = params?.urn
1294 | def ssdpPath = params?.ssdpPath
1295 | def mac = params?.mac
1296 | def uuid = params?.uuid
1297 |
1298 | // FIXME: need to refactor this.
1299 | // now update our static state
1300 | state.ip = dni.split(":").first()
1301 | state.port = dni.split(":").last()
1302 |
1303 | // Set URN for the child device
1304 | state.urn = urn
1305 |
1306 | try {
1307 | // Update device by its MAC address if the DNI changes
1308 | def children = getChildDevices()
1309 | children.each {
1310 | def suffix = ""
1311 | // The primary device has no suffix ":armedAway" etc.
1312 | if (it.deviceNetworkId != dkey) {
1313 | suffix = ":${it.deviceNetworkId.split(":").last().trim()}"
1314 | } else {
1315 | // must be the parent lets udpate the data for it.
1316 | it.updateDataValue("mac", mac)
1317 | it.updateDataValue("urn", urn)
1318 | it.updateDataValue("ssdpUSN", uuid)
1319 | it.updateDataValue("ssdpPath", ssdpPath)
1320 | }
1321 | it.setDeviceNetworkId("${dni}${suffix}")
1322 |
1323 | // FIXME: We also need to update the Name and Label but how?
1324 | // what else?
1325 | }
1326 |
1327 | errors << "Success updating ${dkey} to ${dni}"
1328 | } catch (e) {
1329 | log.error("There was an error (${e}) when trying " +
1330 | "to relink device ${dkey} to ${dni}")
1331 | errors << "Error relinking ${dkey} to ${dni}"
1332 | }
1333 | } else {
1334 |
1335 | // get the active device for info display
1336 | def d = getChildDevice(getDeviceKey())
1337 | if (!d) {
1338 | log.warn("page_relink_update: Could not find primary" +
1339 | " device for '${getDeviceKey()}'.")
1340 | return
1341 | }
1342 |
1343 | // Build heading vars for current active device.
1344 | def dni = getDeviceKey()
1345 | def urn = d.getDeviceDataByName("urn")
1346 | def ssdpPath = d.getDeviceDataByName("ssdpPath")
1347 | def mac = d.getDeviceDataByName("mac")
1348 | def uuid = d.getDeviceDataByName("ssdpUSN")
1349 |
1350 | section(szt("page_relink_section_active_device",
1351 | "\ndni: ${dni}\nurn: ${urn}\nssdpPath: ${ssdpPath}\n" +
1352 | "mac: ${mac}\nusn: ${uuid}")) {
1353 | href(
1354 | name: "href_select_device_relink_update",
1355 | title: szt("tap_here"),
1356 | description: szt("page_select_device_desc"),
1357 | required: false,
1358 | page: "page_select_device"
1359 | )
1360 | }
1361 |
1362 | // Find the discovered device with a matching
1363 | // dni XXXXXXXX:XXXX in input_selected_devices
1364 | d = \
1365 | getDevices().find { k, v ->
1366 | "${v.ip}:${v.port}" == "${input_selected_devices}"
1367 | }
1368 |
1369 | if (d) {
1370 | // Build heading vars for UI selected device.
1371 |
1372 | dni = getDeviceKey(d.value.ip, d.value.port)
1373 | urn = getHostAddressFromDNI(input_selected_devices)
1374 | ssdpPath = d.value.ssdpPath
1375 | mac = d.value.mac
1376 | uuid = d.value.ssdpUSN
1377 |
1378 | section(szt("page_relink_section_selected_device",
1379 | "\ndni: ${dni}\nurn: ${urn}\nssdpPath: ${ssdpPath}\n" +
1380 | "mac: ${mac}\nusn: ${uuid}")) {
1381 | href(
1382 | name: "href_confirm_relink_update",
1383 | title: szt("tap_here"),
1384 | description: szt("info_confirm_relink_update", "${urn}"),
1385 | required: false,
1386 | page: "page_relink_update",
1387 | params: [
1388 | confirm: true,
1389 | dni: dni,
1390 | urn: urn,
1391 | ssdpPath: ssdpPath,
1392 | mac: mac,
1393 | uuid: uuid
1394 | ]
1395 | )
1396 | }
1397 | }
1398 |
1399 | section(szt("section_monitor_integration")) {
1400 | input(
1401 | name: "monIntegration",
1402 | type: "bool",
1403 | defaultValue: true,
1404 | submitOnChange: true,
1405 | title: szt("monIntegration${monitor_suffix}")
1406 | )
1407 | input(
1408 | name: "monChangeStatus",
1409 | type: "bool",
1410 | defaultValue: true,
1411 | submitOnChange: true,
1412 | title: szt("monChangeStatus${monitor_suffix}")
1413 | )
1414 | }
1415 | section(szt("section_zone_sensor_settings")) {
1416 | input(
1417 | name: "defaultSensorToClosed",
1418 | type: "bool",
1419 | defaultValue: true,
1420 | submitOnChange: true,
1421 | title: szt("defaultSensorToClosed")
1422 | )
1423 | }
1424 | }
1425 | section("") {
1426 | paragraph message
1427 | errors.each {
1428 | error->
1429 | paragraph(error)
1430 | }
1431 | }
1432 |
1433 | section_back_note()
1434 | }
1435 | }
1436 |
1437 | /*** Standard service callbacks ***/
1438 |
1439 | /**
1440 | * installed()
1441 | */
1442 | def installed() {
1443 | log.trace "installed"
1444 | if (debug) log.debug "Installed with settings: ${settings}"
1445 |
1446 | // initialize everything
1447 | initialize()
1448 | }
1449 |
1450 | /**
1451 | * updated()
1452 | */
1453 | def updated() {
1454 | log.trace "updated"
1455 | if (debug) log.debug "Updated with settings: ${settings}"
1456 |
1457 | // re initialize everything
1458 | initialize()
1459 | }
1460 |
1461 | /**
1462 | * uninstalled()
1463 | */
1464 | def uninstalled() {
1465 | log.trace "uninstalled"
1466 |
1467 | // disable all scheduling and subscriptions
1468 | unschedule()
1469 |
1470 | // remove all the devices and children
1471 | def devices = getAllChildDevices()
1472 | devices.each {
1473 | try {
1474 | log.debug "deleting child device: ${it.deviceNetworkId}"
1475 | deleteChildDevice(it.deviceNetworkId)
1476 | } catch (Exception e) {
1477 | log.trace("exception while uninstalling: ${e}")
1478 | }
1479 | }
1480 | }
1481 |
1482 | /**
1483 | * initialize called upon update and at startup
1484 | * Add subscriptions and schdules
1485 | * Create our default state
1486 | */
1487 | def initialize() {
1488 | log.trace "initialize"
1489 |
1490 | // unsubscribe from everything
1491 | unsubscribe()
1492 |
1493 | // remove all schedules
1494 | unschedule()
1495 |
1496 | // Create our default state values
1497 | state.lastMONStatus = null
1498 | state.lastAlarmDecoderStatus = null
1499 |
1500 | // Network and Monitor subscriptions
1501 | initSubscriptions()
1502 |
1503 | // if a device in the GUI is selected then add it.
1504 | if (input_selected_devices) {
1505 | addExistingDevices()
1506 | }
1507 |
1508 | // Device handler -> service subscriptions
1509 | configureDeviceSubscriptions()
1510 |
1511 | // keep us subscribed to notifications
1512 | getAllChildDevices().each {
1513 | device->
1514 | // Only refresh the main device that has a panel_state
1515 | def device_type = device.getTypeName()
1516 | if (device_type == "AlarmDecoder network appliance") {
1517 | if (debug)
1518 | log.debug("initialize: Found device refresh subscription.")
1519 | device.subscribeNotifications()
1520 | }
1521 | }
1522 | }
1523 |
1524 | /*** Event handlers ***/
1525 |
1526 | /**
1527 | * locationHandler(evt)
1528 | * Local SSDP/UPNP network messages sent on UDP port 1900
1529 | * and NOTIFICATIONS sent to the hub.localSrvPortTCP
1530 | * will be captured here. The messages will then if valid be parsed
1531 | * into a parsedEvent Map.
1532 | *
1533 | * Test from the AlarmDecoder Appliance:
1534 | * curl
1535 | *
1536 | */
1537 | def locationHandler(evt) {
1538 | if (debug)
1539 | log.trace "locationHandler: name: '${evt.name}'"
1540 |
1541 | // only process events with a description.
1542 | if (!evt.description) {
1543 | if (debug)
1544 | log.info("locationHandler: skipping event missing 'description'")
1545 | return
1546 | }
1547 |
1548 | // Parse message into parsedEvent map
1549 | def parsedEvent = parseEventMessage(evt.description)
1550 |
1551 | // UPNP LAN EVENTS on UDP port 1900 from 'AlarmDecoder:1' devices only
1552 | //// parse and update state.devices Map
1553 | if (parsedEvent.ssdpTerm?.contains(SSDPTERM)) {
1554 | def ct = now()
1555 |
1556 | if (debug)
1557 | log.debug "locationHandler: received ssdpTerm match."
1558 |
1559 | // Pre fill parsed event object with hubId the event was from.
1560 | parsedEvent << ["hubId": evt?.hubId]
1561 |
1562 | // Add a timestamp for garbage collection
1563 | parsedEvent << ["ts": now()]
1564 |
1565 | // get our ssdp discovery results array.
1566 | def alarmdecoders = getDevices()
1567 |
1568 | // Look at all entries and remove expired ones
1569 | def garbage = []
1570 | alarmdecoders.each { k, v ->
1571 | if (v.ts) {
1572 | // Expire if not seen for 5minutes
1573 | if ((ct - v.ts) / 1000 > (5 * 60)) {
1574 | log.warn("locationHandler: removing expired ssdp discovery: " +
1575 | "mac:${v.mac} ip:${v.ip}")
1576 | garbage << k
1577 | } else {
1578 | if (debug)
1579 | log.warn("locationHandler: ${k} discovery last " +
1580 | "seen: ${(ct-v.ts)/1000}s ago mac:${v.mac} ip:${v.ip})")
1581 | }
1582 | } else {
1583 | // no ts so set one
1584 | log.warn("locationHandler: ts not found for " +
1585 | "mac:${v.mac} ip:${v.ip} adding.")
1586 | v.ts = now()
1587 | }
1588 | }
1589 | // finally remove them
1590 | garbage.each { v ->
1591 | alarmdecoders.remove(v)
1592 | }
1593 |
1594 | // add/update the device in state.devices with local discovered devices.
1595 | alarmdecoders << ["${parsedEvent.ssdpUSN.toString()}": parsedEvent]
1596 |
1597 | if (debug)
1598 | log.debug("locationHandler: alarmdecoders found: ${alarmdecoders}")
1599 |
1600 | } else {
1601 |
1602 | // Content type already parsed here.
1603 | def type = parsedEvent.contenttype
1604 |
1605 | if (debug)
1606 | log.debug("locationHandler: HTTP request type:${type} " +
1607 | "body:${parsedEvent?.body} headers:${parsedEvent?.headers}")
1608 |
1609 | // XML PUSH data
1610 | if (type?.contains("xml")) {
1611 | // get our primary device the Alarmdecoder UI.
1612 | def d = getChildDevice("${getDeviceKey()}")
1613 | if (d) {
1614 | if (d.getDeviceDataByName("mac") == parsedEvent.mac) {
1615 | if (debug)
1616 | log.debug("push_update_alarmdecoders: Found device parse xml data.")
1617 |
1618 | d.parse_xml(parsedEvent?.body).each {
1619 | e-> d.sendEvent(e)
1620 | }
1621 |
1622 | return
1623 | }
1624 | }
1625 | }
1626 |
1627 | // Unkonwn silently ignore
1628 | if (debug)
1629 | log.debug("locationHandler: ignoring unknown message from " +
1630 | "name:${evt.name} parsedEvent: ${parsedEvent}")
1631 | }
1632 | }
1633 |
1634 | /**
1635 | * Handle remote web requests for http://somegraph/update
1636 | */
1637 | def webserviceUpdate() {
1638 | log.trace "webserviceUpdate"
1639 | refresh_alarmdecoders()
1640 | return [status: "OK"]
1641 | }
1642 |
1643 | /**
1644 | * Handle our child device action button events
1645 | * sets Contact attributes of the alarmdecoder smoke device
1646 | */
1647 | def actionButton(id) {
1648 |
1649 | // grab our primary AlarmDecoder device object
1650 | def d = getChildDevice("${getDeviceKey()}")
1651 | if (debug) log.debug("actionButton: desc=${id} dev=${d}")
1652 |
1653 | if (!d) {
1654 | log.error("actionButton: Could not find primary dev. '${getDeviceKey()}'.")
1655 | return
1656 | }
1657 |
1658 | /* FIXME: Need a pin code or some way to trust the request. */
1659 | if (CREATE_DISARM) {
1660 | if (id.contains(":disarm")) {
1661 | d.disarm()
1662 | }
1663 | }
1664 | if (id.contains(":exit")) {
1665 | d.exit()
1666 | }
1667 | if (id.contains(":armAway")) {
1668 | d.arm_away()
1669 | }
1670 | if (id.contains(":armStay")) {
1671 | d.arm_stay()
1672 | }
1673 | if (id.contains(":chimeMode")) {
1674 | d.chime()
1675 | }
1676 | if (id.contains(":alarmPanic")) {
1677 | d.panic()
1678 | }
1679 | if (id.contains(":alarmAUX")) {
1680 | d.aux()
1681 | }
1682 | if (id.contains(":alarmFire")) {
1683 | d.fire()
1684 | }
1685 | // Turn off alarm bell if pushed
1686 | if (id.contains(":alarmBell")) {
1687 | def cd = getChildDevice("${id}")
1688 | if (!cd) {
1689 | log.info("actionButton: Could not clear '${id}'.")
1690 | return
1691 | }
1692 | _sendEventTranslate(cd, "off")
1693 | }
1694 | if (id.contains(":CID-")) {
1695 | def cd = getChildDevice("${id}")
1696 | if (!cd) {
1697 | log.info("actionButton: Could not clear device '${id}'.")
1698 | return
1699 | }
1700 | _sendEventTranslate(cd, "off")
1701 | }
1702 | }
1703 |
1704 | /**
1705 | * send event to smokeAlarm device to set state [detected, clear]
1706 | */
1707 | def smokeSet(evt) {
1708 | if (debug) log.debug("smokeSet: desc=${evt.value}")
1709 |
1710 | def d = getChildDevices().find {
1711 | it.deviceNetworkId.contains(":smokeAlarm")
1712 | }
1713 |
1714 | if (!d) {
1715 | log.info("smokeSet: Could not find 'SmokeAlarm' device.")
1716 | return
1717 | }
1718 | _sendEventTranslate(d, (evt.value == "detected" ? "on" : "off"))
1719 |
1720 | d = getChildDevice("${getDeviceKey()}:alarmFireStatus")
1721 | if (!d) {
1722 | log.info("smokeSet: Could not find 'alarmFireStatus' device.")
1723 | } else {
1724 | _sendEventTranslate(d, (evt.value == "detected" ? "on" : "off"))
1725 | }
1726 | }
1727 |
1728 | /**
1729 | * send event to armAway device to set state
1730 | */
1731 | def armAwaySet(evt) {
1732 | if (debug) log.debug("armAwaySet ${evt.value}")
1733 | def d = getChildDevice("${getDeviceKey()}:armAway")
1734 | if (!d) {
1735 | log.info("armAwaySet: Could not find 'armAway' device.")
1736 | } else {
1737 | _sendEventTranslate(d, evt.value)
1738 | }
1739 |
1740 | d = getChildDevice("${getDeviceKey()}:armAwayStatus")
1741 | if (!d) {
1742 | log.info("armAwaySet: Could not find 'armAwayStatus' device.")
1743 | } else {
1744 | _sendEventTranslate(d, evt.value)
1745 | }
1746 | }
1747 |
1748 | /**
1749 | * send event to armStay device to set state
1750 | */
1751 | def armStaySet(evt) {
1752 | if (debug) log.debug("armStaySet ${evt.value}")
1753 | def d = getChildDevice("${getDeviceKey()}:armStay")
1754 | if (!d) {
1755 | log.info("armStaySet: Could not find 'armStay' device.")
1756 | } else {
1757 | d.sendEvent(
1758 | name: "switch",
1759 | value: evt.value,
1760 | isStateChange: true,
1761 | filtered: true
1762 | )
1763 | }
1764 |
1765 | d = getChildDevice("${getDeviceKey()}:armStayStatus")
1766 | if (!d) {
1767 | log.info("armStaySet: Could not find 'armStayStatus' device.")
1768 | } else {
1769 | _sendEventTranslate(d, evt.value)
1770 | }
1771 | }
1772 |
1773 | /**
1774 | * send event to alarmbell indicator device to set state
1775 | */
1776 | def alarmBellSet(evt) {
1777 | if (debug) log.debug("alarmBellSet ${evt.value}")
1778 | def d = getChildDevice("${getDeviceKey()}:alarmBell")
1779 | if (!d) {
1780 | log.info("alarmBellSet: Could not find 'alarmBell' device.")
1781 | } else {
1782 | _sendEventTranslate(d, evt.value)
1783 | }
1784 |
1785 | d = getChildDevice("${getDeviceKey()}:alarmBellStatus")
1786 | if (!d) {
1787 | log.info("alarmBellSet: Could not find device 'alarmBellStatus'")
1788 | } else {
1789 | _sendEventTranslate(d, evt.value)
1790 | }
1791 | }
1792 |
1793 | /**
1794 | * send event to chime indicator device to set state
1795 | */
1796 | def chimeSet(evt) {
1797 | /* if (debug)*/
1798 | log.debug("chimeSet ${evt.value}")
1799 | def d = getChildDevice("${getDeviceKey()}:chimeMode")
1800 | if (!d) {
1801 | log.info("chimeSet: Could not find device 'chimeMode'")
1802 | } else {
1803 | _sendEventTranslate(d, evt.value)
1804 | }
1805 |
1806 | d = getChildDevice("${getDeviceKey()}:chimeModeStatus")
1807 | if (!d) {
1808 | log.info("chimeSet: Could not find device 'chimeModeStatus'")
1809 | } else {
1810 | _sendEventTranslate(d, evt.value)
1811 | }
1812 | }
1813 |
1814 | /**
1815 | * send event to exit indicator device to set state
1816 | */
1817 | def exitSet(evt) {
1818 | if (debug) log.debug("exitSet ${evt.value}")
1819 | def d = getChildDevice("${getDeviceKey()}:exit")
1820 | if (!d) {
1821 | log.info("exitSet: Could not find device 'exit'")
1822 | } else {
1823 | _sendEventTranslate(d, evt.value)
1824 | }
1825 |
1826 | d = getChildDevice("${getDeviceKey()}:exitStatus")
1827 | if (!d) {
1828 | log.info("exitSet: Could not find device 'exitStatus'")
1829 | } else {
1830 | _sendEventTranslate(d, evt.value)
1831 | }
1832 | }
1833 |
1834 | /**
1835 | * send event to perimeter only indicator device to set state
1836 | */
1837 | def perimeterOnlySet(evt) {
1838 | if (debug) log.debug("perimeterOnlySet ${evt.value}")
1839 | def d = getChildDevice("${getDeviceKey()}:perimeterOnlyStatus")
1840 | if (!d) {
1841 | log.info("perimeterOnlySet: Could not find device 'perimeterOnly'")
1842 | return
1843 | }
1844 | _sendEventTranslate(d, evt.value)
1845 | }
1846 |
1847 | /**
1848 | * send event to entry delay off indicator device to set state
1849 | */
1850 | def entryDelayOffSet(evt) {
1851 | if (debug) log.debug("entryDelayOffSet ${evt.value}")
1852 | def d = getChildDevice("${getDeviceKey()}:entryDelayOffStatus")
1853 | if (!d) {
1854 | log.info("entryDelayOffSet: Could not find device 'entryDelayOff'")
1855 | return
1856 | }
1857 | _sendEventTranslate(d, evt.value)
1858 | }
1859 |
1860 |
1861 | /**
1862 | * send event to bypass status device to set state
1863 | */
1864 | def bypassSet(evt) {
1865 | if (debug) log.debug("bypassSet ${evt.value}")
1866 | def d = getChildDevice("${getDeviceKey()}:bypassStatus")
1867 | if (!d) {
1868 | log.info("bypassSet: Could not find device 'bypassStatus'")
1869 | return
1870 | }
1871 | _sendEventTranslate(d, evt.value)
1872 | }
1873 |
1874 | /**
1875 | * send event to ready status device to set state
1876 | */
1877 | def readySet(evt) {
1878 | if (debug) log.debug("readySet ${evt.value}")
1879 | def d = getChildDevice("${getDeviceKey()}:readyStatus")
1880 | if (!d) {
1881 | log.info("readySet: Could not find 'readyStatus' device.")
1882 | return
1883 | }
1884 | _sendEventTranslate(d, evt.value)
1885 | }
1886 |
1887 | /**
1888 | * send event to disarm status device to set state
1889 | */
1890 | def disarmSet(evt) {
1891 | if (debug) log.debug("disarmSet ${evt.value}")
1892 | def d = getChildDevice("${getDeviceKey()}:disarm")
1893 | if (!d) {
1894 | log.info("disarmSet: Could not find 'disarm' device.")
1895 | } else {
1896 | _sendEventTranslate(d, evt.value)
1897 | }
1898 | }
1899 |
1900 | /**
1901 | * send CID event to the correct device if one exists
1902 | * evt.value example !LRR:001,1,CID_1406,ff
1903 | */
1904 | def cidSet(evt) {
1905 | log.info("cidSet ${evt.value}")
1906 |
1907 | // get our CID state and number
1908 | def parts = evt.value.split(',')
1909 |
1910 | // 1 digit QUALIFIER 1 = Event or Open, 3 = Restore or Close
1911 | def cidstate = (parts[2][-4.. - 4] == "1") ? "on" : "off"
1912 |
1913 | // 3 digit CID number
1914 | def cidnum = parts[2][-3.. - 1]
1915 |
1916 | // the CID report value. Zone # or User # or ...
1917 | def cidvalue = parts[0].split(':')[1]
1918 |
1919 | // the partition # with 0 being system
1920 | def partition = parts[1].toInteger()
1921 |
1922 | if (debug)
1923 | log.debug("cidSet num:${cidnum} part: ${partition} " +
1924 | "state:${cidstate} val:${cidvalue}")
1925 |
1926 | def sent = false
1927 | def rawmsg = evt.value
1928 | def device_name = "CID-${cidnum}-${partition}-${cidvalue}"
1929 | def children = getChildDevices()
1930 | children.each {
1931 | if (it.deviceNetworkId.contains(":CID-")) {
1932 |
1933 | def match = getDeviceNamePart(it)
1934 |
1935 | // replace ? with . non regex
1936 | match = match.replace("?", ".")
1937 |
1938 | if (device_name =~ /${match}/) {
1939 | if (debug)
1940 | log.debug("cidSet device: ${device_name} matches ${match} " +
1941 | "sending state ${cidstate}")
1942 |
1943 | _sendEventTranslate(it, cidstate)
1944 |
1945 | sent = true
1946 | } else {
1947 | if (debug)
1948 | log.debug("cidSet device: ${device_name} no match ${match}")
1949 | }
1950 | }
1951 | }
1952 |
1953 | if (!sent) {
1954 | log.error("cidSet: Could not find " +
1955 | "'CID-${cidnum}-${partition}-${cidvalue}|XXX' device.")
1956 | return
1957 | }
1958 | }
1959 |
1960 | /**
1961 | * send RFX event to the correct device if one exists
1962 | * evt.value example raw !RFX:123123,a0
1963 | * eventmessage 123123:0:0:1:1:0:0
1964 | * 01020304:1388:RFX-123123-?-?-1-?-?-?
1965 | */
1966 | def rfxSet(evt) {
1967 | log.info("rfxSet ${evt.value}")
1968 |
1969 | // get our RFX state and number
1970 | def parts = evt.value.split(':')
1971 |
1972 | def sn = parts[0]
1973 | def bat = parts[1]
1974 | def supv = parts[2]
1975 | def loop0 = parts[3]
1976 | def loop1 = parts[4]
1977 | def loop2 = parts[5]
1978 | def loop3 = parts[6]
1979 |
1980 | if (debug)
1981 | log.info("rfxSet sn:${sn} bat: ${bat} sukpv:${supv} loop0:${loop0} " +
1982 | "loop1:${loop1} loop2:${loop2} loop3:${loop3}")
1983 |
1984 | def sent = false
1985 |
1986 | def device_name = "RFX-${sn}-${bat}-${supv}-" +
1987 | "${loop0}-${loop1}-${loop2}-${loop3}"
1988 |
1989 | def children = getChildDevices()
1990 | children.each {
1991 | if (it.deviceNetworkId.contains(":RFX-")) {
1992 |
1993 | def sp = getDeviceNamePart(it).split("-")
1994 |
1995 | def match = sp[0] + "-" + sp[1] + "-*"
1996 | if (device_name =~ /${match}/) {
1997 | def tot = 0
1998 | if (sp[2] == "1" && bat == "1") {
1999 | tot++
2000 | }
2001 | if (sp[3] == "1" && supv == "1") {
2002 | tot++
2003 | }
2004 | if (sp[4] == "1" && loop0 == "1") {
2005 | tot++
2006 | }
2007 | if (sp[5] == "1" && loop1 == "1") {
2008 | tot++
2009 | }
2010 | if (sp[6] == "1" && loop2 == "1") {
2011 | tot++
2012 | }
2013 | if (sp[7] == "1" && loop3 == "1") {
2014 | tot++
2015 | }
2016 |
2017 | _sendEventTranslate(it, (tot > 0 ? "on" : "off"))
2018 |
2019 | if (debug)
2020 | log.info("rfxSet device: ${device_name} matches ${match} " +
2021 | "sending state ${tot}")
2022 |
2023 | sent = true
2024 |
2025 | } else {
2026 | if (debug) log.info("rfxSet device: ${device_name} no match ${match}")
2027 | }
2028 | }
2029 | }
2030 |
2031 | if (!sent) {
2032 | log.warn("rfxSet: Could not find '${device_name}|XXX' device.")
2033 | return
2034 | }
2035 | }
2036 |
2037 | /**
2038 | * Handle Device Command addZone()
2039 | * add a zone during post install to keep it async
2040 | */
2041 | def addZone(evt) {
2042 |
2043 | def i = evt.value
2044 | log.info("App Event: addZone ${i}")
2045 |
2046 | // do not create devices if testing. Real PITA to delete them
2047 | // every time. ST needs to add a way to delete multiple devices at once.
2048 | if (NOCREATEDEV) {
2049 | log.warn "addZone: NOCREATEDEV enabled skipping ${evt.data}."
2050 | return
2051 | }
2052 |
2053 | def d = getChildDevice("${evt.data}")
2054 | if (d) {
2055 | log.warn "addZone: Already found zone ${i} device ${evt.data} skipping."
2056 | return
2057 | }
2058 |
2059 | try {
2060 | def zone_switch = \
2061 | addChildDevice(
2062 | APPNAMESPACE,
2063 | "AlarmDecoder virtual contact sensor",
2064 | "${evt.data}",
2065 | state.hubId,
2066 | [
2067 | name: "${evt.data}",
2068 | label: "${sname} Zone Sensor #${i}",
2069 | completedSetup: true
2070 | ]
2071 | )
2072 | def sensorValue = "open"
2073 | if (settings.defaultSensorToClosed == true) {
2074 | sensorValue = "closed"
2075 | }
2076 |
2077 | // Set default contact state.
2078 | //_sendEventTranslate(zone_switch, (sensorValue == "open" ? "on" : "off"))
2079 | } catch (e) {
2080 | log.error "There was an error (${e}) when trying to addZone ${i}"
2081 | }
2082 | }
2083 |
2084 |
2085 | /**
2086 | * Handle Device Command zoneOn()
2087 | * sets Contact attributes of the alarmdecoder device to open/closed
2088 | */
2089 | def zoneOn(evt) {
2090 | if (debug) log.debug("zoneOn: desc=${evt.value}")
2091 |
2092 | // Find all :switch devices with a matching zone the event.
2093 | def d = getChildDevices().findAll {
2094 | it.deviceNetworkId.contains(":switch") &&
2095 | it.getDataValue("zone") == evt.value
2096 | }
2097 |
2098 | if (d) {
2099 | // Send the event to all devices that had a matching zone value.
2100 | d.each {
2101 | _sendEventTranslate(it, ("on"))
2102 | }
2103 | } else {
2104 | log.warn "zoneOn: Virtual device with zone #${evt.value} not found."
2105 | }
2106 | }
2107 |
2108 | /**
2109 | * Handle Device Command zoneOff()
2110 | * sets Contact attributes of the alarmdecoder device to open/closed
2111 | */
2112 | def zoneOff(evt) {
2113 | if (debug) log.debug("zoneOff: desc=${evt.value}")
2114 |
2115 | def d = getChildDevices().findAll {
2116 | it.deviceNetworkId.contains(":switch") &&
2117 | it.getDataValue("zone") == evt.value
2118 | }
2119 |
2120 | if (d) {
2121 | // Send the event to all devices that had a matching zone value.
2122 | d.each {
2123 | _sendEventTranslate(it, ("off"))
2124 | }
2125 | } else {
2126 | log.warn "zoneOn: Virtual device with zone #${evt.value} not found."
2127 | }
2128 | }
2129 |
2130 | /**
2131 | * Handle SmartThings Smart Home Monitor(SHM) or Hubitat Safety Monitor (HSM)
2132 | * events and update the UI of the App.
2133 | */
2134 | def monitorAlarmHandler(evt) {
2135 | if (settings.monIntegration == false)
2136 | return
2137 |
2138 |
2139 | if (state.lastMONStatus != evt.value) {
2140 | if (debug)
2141 | log.debug("monitorAlarmHandler -- update lastMONStatus " +
2142 | "to ${evt.value} from ${state.lastMONStatus}")
2143 |
2144 | // Update last known MON state
2145 | state.lastMONStatus = evt.value
2146 |
2147 | getAllChildDevices().each {
2148 | device->
2149 | // Only refresh the main device that has a panel_state
2150 | def device_type = device.getTypeName()
2151 | if (device_type == "AlarmDecoder network appliance") {
2152 | if (debug)
2153 | log.debug("monitorAlarmHandler DEBUG-- ${device.deviceNetworkId}")
2154 |
2155 | /* SmartThings */
2156 | if (isSmartThings()) {
2157 | if (evt.value == "away" || evt.value == "armAway") {
2158 | // do not send if already in that state.
2159 | if (!device.getStateValue("panel_armed") &&
2160 | !device.getStateValue("panel_armed_stay")) {
2161 | device.arm_away()
2162 | } else {
2163 | log.trace "monitorAlarmHandler -- no send arm_away already set"
2164 | }
2165 | } else if (evt.value == "stay" || evt.value == "armHome") {
2166 | // do not send if already in that state.
2167 | if (!device.getStateValue("panel_armed") &&
2168 | !device.getStateValue("panel_armed_stay")) {
2169 | device.arm_stay()
2170 | } else {
2171 | log.trace "monitorAlarmHandler -- no send arm_stay already set"
2172 | }
2173 | } else if (evt.value == "off" || evt.value == "disarm") {
2174 | // do not send if already in that state.
2175 | if (device.getStateValue("panel_armed") ||
2176 | device.getStateValue("panel_armed_stay")) {
2177 | device.disarm()
2178 | } else {
2179 | log.trace "monitorAlarmHandler -- no send disarm already set"
2180 | }
2181 | } else
2182 | log.debug "Unknown SHM alarm value: ${evt.value}"
2183 | }
2184 | /* Hubitat */
2185 | else if (isHubitat()) {
2186 | if (evt.value == "armedAway") {
2187 | // do not send if already in that state.
2188 | if (!device.getStateValue("panel_armed") &&
2189 | !device.getStateValue("panel_armed_stay")) {
2190 | device.arm_away()
2191 | } else {
2192 | log.trace "monitorAlarmHandler -- no send arm_away already set"
2193 | }
2194 | } else if (evt.value == "armedHome") {
2195 | // do not send if already in that state.
2196 | if (!device.getStateValue("panel_armed") &&
2197 | !device.getStateValue("panel_armed_stay")) {
2198 | device.arm_stay()
2199 | } else {
2200 | log.trace "monitorAlarmHandler -- no send arm_stay already set"
2201 | }
2202 | } else if (evt.value == "disarmed") {
2203 | // do not send if already in that state.
2204 | if (device.getStateValue("panel_armed") ||
2205 | device.getStateValue("panel_armed_stay")) {
2206 | device.disarm()
2207 | } else {
2208 | log.trace "monitorAlarmHandler -- no send disarm already " +
2209 | "set ${device.getStateValue('panel_armed')} " +
2210 | "${device.getStateValue('panel_armed_stay')}"
2211 | }
2212 | } else
2213 | log.debug "Unknown HSM alarm value: ${evt.value}"
2214 | }
2215 | }
2216 | }
2217 | }
2218 | }
2219 |
2220 | /**
2221 | * Handle Alarm events from the AlarmDecoder and
2222 | * send them back to the the Monitor API to update the
2223 | * status of the alarm panel
2224 | */
2225 | def alarmdecoderAlarmHandler(evt) {
2226 | if (settings.monIntegration == false || settings.monChangeStatus == false)
2227 | return
2228 |
2229 | if (debug)
2230 | log.debug("alarmdecoderAlarmHandler -- update lastAlarmDecoderStatus " +
2231 | "to ${evt.value} from ${state.lastAlarmDecoderStatus}")
2232 |
2233 | state.lastAlarmDecoderStatus = evt.value
2234 |
2235 | if (isSmartThings()) {
2236 | /* no traslation needed already [stay,away,off] */
2237 | if (debug)
2238 | log.debug("alarmdecoderAlarmHandler alarmSystemStatus ${evt.value}")
2239 |
2240 | // Update last known MON state
2241 | state.lastMONStatus = evt.value
2242 |
2243 | sendLocationEvent(name: "alarmSystemStatus", value: evt.value)
2244 | } else if (isHubitat()) {
2245 | /* translate to HSM */
2246 | msg = ""
2247 | nstate = ""
2248 | if (evt.value == "stay") {
2249 | msg = "armHome"
2250 | nstate = "armedHome" // prevent loop
2251 | }
2252 | if (evt.value == "away") {
2253 | msg = "armAway"
2254 | nstate = "armedAway" // prevent loop
2255 | }
2256 | if (evt.value == "off") {
2257 | msg = "disarm"
2258 | nstate = "disarmed" // prevent loop
2259 | }
2260 |
2261 | if (debug)
2262 | log.debug("alarmdecoderAlarmHandler: hsmSetArm ${msg} " +
2263 | "last ${state.lastMONStatus} new ${nstate}")
2264 |
2265 | // Update last known MON state
2266 | state.lastMONStatus = nstate
2267 |
2268 | // Notify external MON of the change
2269 | sendLocationEvent(name: "hsmSetArm", value: msg)
2270 | } else {
2271 | log.warn("alarmdecoderAlarmHandler: monttype? evt:value: ${evt.value} " +
2272 | " lastAlarmDecoderStatus: ${state.lastAlarmDecoderStatus}")
2273 | }
2274 | }
2275 |
2276 | /*** Utility/Misc ***/
2277 |
2278 | /*
2279 | * determines if the app is running under SmartThings
2280 | */
2281 | def isSmartThings() {
2282 | return physicalgraph?.device?.HubAction;
2283 | }
2284 |
2285 | /*
2286 | * determines if the app is running under Hubitat
2287 | */
2288 | def isHubitat() {
2289 | return hubitat?.device?.HubAction;
2290 | }
2291 |
2292 | /**
2293 | * Enable primary network and system subscriptions
2294 | */
2295 | def initSubscriptions() {
2296 | // subscribe to the Smart Home Manager api for alarm status events
2297 | if (debug) log.debug("initSubscriptions: Subscribe to handlers")
2298 |
2299 | if (isSmartThings()) {
2300 | subscribe(location, "alarmSystemStatus", monitorAlarmHandler)
2301 | } else if (isHubitat()) {
2302 | subscribe(location, "hsmStatus", monitorAlarmHandler)
2303 | }
2304 |
2305 | // subscribe to add zone handler
2306 | subscribe(app, addZone)
2307 |
2308 | // subscribe to local LAN messages to this HUB on TCP port 39500 and
2309 | // UPNP UDP port 1900
2310 | subscribe(location, null, locationHandler, [filterEvents: false])
2311 | }
2312 |
2313 | /**
2314 | * Called by page_discover page periodically
2315 | * sends a UPNP discovery message from the HUB
2316 | * to the local network
2317 | */
2318 | def discover_alarmdecoder() {
2319 | if (debug) log.debug("discover_alarmdecoder")
2320 | def haobj =
2321 | getHubAction("lan discovery ${SSDPTERM}}")
2322 |
2323 | sendHubCommand(haobj)
2324 | }
2325 |
2326 | /*
2327 | * sendVerify sends a message to the HUB.
2328 | */
2329 | def sendVerify(DNI, ssdpPath) {
2330 |
2331 | String ip = getHostAddressFromDNI(DNI)
2332 |
2333 | if (debug)
2334 | log.debug("verifyAlarmDecoder: ${DNI} ssdpPath: ${ssdpPath} ip: ${ip}")
2335 |
2336 | def haobj =
2337 | getHubAction(
2338 | [method: "GET", path: ssdpPath, headers: [Host: ip, Accept: "*/*"]],
2339 | DNI
2340 | )
2341 |
2342 | sendHubCommand(haobj)
2343 | }
2344 |
2345 | /**
2346 | * Call refresh() on the AlarmDecoder parent device object.
2347 | * This will force the HUB to send a REST API request to the AlarmDecoder
2348 | * Network Appliance. and get back the current status of the AlarmDecoder.
2349 | */
2350 | def refresh_alarmdecoders() {
2351 | if (debug) log.debug("refresh_alarmdecoders")
2352 |
2353 | // just because it seems to get lost.
2354 | initSubscriptions()
2355 |
2356 | getAllChildDevices().each {
2357 | device->
2358 | // Only refresh the main device that has a panel_state
2359 | def device_type = device.getTypeName()
2360 | if (device_type == "AlarmDecoder network appliance") {
2361 | def apikey = device._get_api_key()
2362 | if (apikey) {
2363 | device.refresh()
2364 | } else {
2365 | log.error("refresh_alarmdecoders no API KEY for: " +
2366 | "${device} @ ${device.getDataValue("urn")}")
2367 | }
2368 | }
2369 | }
2370 | }
2371 |
2372 | /**
2373 | * return the list of known devices and initialize the list if needed.
2374 | *
2375 | * FIXME: SM20180315:
2376 | * This uses the ssdpUSN as the key when we also use DNI
2377 | * Why not just use DNI all over or ssdpUSN. Keep it consistent.
2378 | * We get ssdpUSN from our UPNP discovery messages on port 1900
2379 | * and then we get DNI messages from our GET requests to the
2380 | * alarmdecoder web services on port 5000. We can also get DNI
2381 | * from Notification events we subscribe to when the AlarmDecoder
2382 | * sends us requests on port 39500. Easy way is to use DNI as we get
2383 | * it every time from all requests. Downside is we can not have more
2384 | * than one AlarmDecoder per IP:PORT. This seems ok to me for now.
2385 | *
2386 | *
2387 | * state.devices structure
2388 | * [
2389 | * uuid:0c510e98-8ce0-11e7-81a5-XXXXXXXXXXXXXX:
2390 | * [
2391 | * port:1388,
2392 | * ssdpUSN:uuid:0c510e98-8ce0-11e7-81a5-XXXXXXXXXXXXXX,
2393 | * devicetype:04,
2394 | * mac:XXXXXXXXXX02,
2395 | * hub:936de0be-1cb7-4185-9ac9-XXXXXXXXXXXXXX,
2396 | * ssdpPath:http://XXX.XXX.XXX.XXX:5000,
2397 | * ssdpTerm:urn:schemas-upnp-org:device:AlarmDecoder:1,
2398 | * ip:XXXXXXX2
2399 | * ],
2400 | * uuid:592952ba-77b0-11e7-b0c7-XXXXXXXXXXXXXX:
2401 | * [
2402 | * port:1388,
2403 | * ssdpUSN:uuid:592952ba-77b0-11e7-b0c7-XXXXXXXXXXXXXX,
2404 | * devicetype:04,
2405 | * mac:XXXXXXXXXX01,
2406 | * hub:936de0be-1cb7-4185-9ac9-XXXXXXXXXXXXXX,
2407 | * ssdpPath:/static/device_description.xml,
2408 | * ssdpTerm:urn:schemas-upnp-org:device:AlarmDecoder:1,
2409 | * ip:XXXXXXX1
2410 | * ]
2411 | * ]
2412 | *
2413 | */
2414 | def getDevices() {
2415 | if (!state.devices) {
2416 | state.devices = [: ]
2417 | }
2418 | return state.devices
2419 | }
2420 |
2421 | /**
2422 | * Add all devices if triggered by the "Setup And Management" pages.
2423 | */
2424 | def addExistingDevices() {
2425 | if (debug)
2426 | log.debug("addExistingDevices: ${input_selected_devices}")
2427 |
2428 | // resubscribe just in case it was lost
2429 | configureDeviceSubscriptions()
2430 |
2431 | //FIXME: Why? Maybe this returns [] or "" if multi select.
2432 | def selected_devices = input_selected_devices
2433 | if (selected_devices instanceof java.lang.String) {
2434 | selected_devices = [selected_devices]
2435 | } else {
2436 | log.debug("addExistingDevices: FIXME not input_selected_devices not String")
2437 | }
2438 |
2439 | selected_devices.each {
2440 | dni->
2441 | if (debug)
2442 | log.debug("addExistingDevices, getChildDevice(${dni})")
2443 |
2444 | def d = getChildDevice(dni)
2445 |
2446 | if (!d) {
2447 | // Find the discovered device with a matching dni XXXXXXXX:XXXX
2448 | def newDevice = \
2449 | getDevices().find { k, v -> "${v.ip}:${v.port}" == dni
2450 | }
2451 |
2452 | if (debug)
2453 | log.debug("addExistingDevices, devices.find=${newDevice}")
2454 |
2455 | if (newDevice) {
2456 | // FIXME: Save DNI details for filtering
2457 | // This needs to be reviewed.
2458 | // We have this data already so why put into a state var
2459 | state.ip = newDevice.value.ip
2460 | state.port = newDevice.value.port
2461 | state.hubId = newDevice.value.hubId
2462 |
2463 | // Set URN for the child device
2464 | state.urn = convertHexToIP(state.ip) + ":" +
2465 | convertHexToInt(state.port)
2466 |
2467 | if (debug)
2468 | log.debug("AlarmDecoder webapp urn ('${state.urn}') " +
2469 | "hub ('${state.hubId}')")
2470 |
2471 | try {
2472 | // Create device adding the URN to its data object
2473 | d = addChildDevice(APPNAMESPACE,
2474 | "AlarmDecoder network appliance",
2475 | "${getDeviceKey()}",
2476 | state.hubId,
2477 | [
2478 | name: "${getDeviceKey()}",
2479 | label: "${guiname}",
2480 | completedSetup: true,
2481 | /* data associated with this AlarmDecoder */
2482 | data: [
2483 | // save mac address to update if IP / PORT change
2484 | mac: newDevice.value.mac,
2485 | ssdpUSN: newDevice.value.ssdpUSN,
2486 | urn: state.urn,
2487 | ssdpPath: newDevice.value.ssdpPath
2488 | ]
2489 | ]
2490 | )
2491 |
2492 | // Set default device state to notready.
2493 | d.sendEvent(
2494 | name: "panel_state",
2495 | value: "notready",
2496 | isStateChange: true,
2497 | displayed: true
2498 | )
2499 | } catch (e) {
2500 | log.info "Error creating device root device ${guiname}"
2501 | }
2502 | }
2503 | }
2504 |
2505 | // Add zone contact sensors if they do not exist.
2506 | // asynchronous to avoid timeout. Apps can only run for 20 seconds or
2507 | // it will be killed.
2508 | for (def i = 0; i < MAX_VIRTUAL_ZONES; i++) {
2509 | if (debug) log.debug("Adding virtual zone sensor ${i}")
2510 | // SmartThings we do out of band with callback
2511 | if (isSmartThings()) {
2512 | sendEvent(
2513 | name: "addZone",
2514 | value: "${i+1}",
2515 | data: "${getDeviceKey()}:switch${i+1}"
2516 | )
2517 | }
2518 | // Callbacks to local events seem to not work on HT
2519 | else if (isHubitat()) {
2520 | if (debug)
2521 | log.warn("NOTE: Hubitate calling addZone directly")
2522 | def evt = [value: "${i+1}", data: "${getDeviceKey()}:switch${i+1}"]
2523 | addZone(evt)
2524 | }
2525 | }
2526 |
2527 | // do not create devices if testing. Real PITA to delete them
2528 | // every time. ST needs to add a way to delete multiple devices at once.
2529 | if (!NOCREATEDEV) {
2530 | // Add Smoke Alarm sensors if it does not exist.
2531 | def cd = \
2532 | getChildDevice("${getDeviceKey()}:smokeAlarm")
2533 | if (!cd) {
2534 | try {
2535 | def nd = \
2536 | addChildDevice(
2537 | APPNAMESPACE,
2538 | "AlarmDecoder virtual smoke alarm",
2539 | "${getDeviceKey()}:smokeAlarm",
2540 | state.hubId,
2541 | [
2542 | name: "${getDeviceKey()}:smokeAlarm",
2543 | label: "${sname} Smoke Alarm",
2544 | completedSetup: true
2545 | ]
2546 | )
2547 | nd.sendEvent(
2548 | name: "smoke",
2549 | value: "clear",
2550 | isStateChange: true,
2551 | displayed: false
2552 | )
2553 | } catch (e) {
2554 | log.info "Error creating device: smokeAlarm"
2555 | }
2556 | } else {
2557 | log.warn "addExistingDevices: Already found device " +
2558 | "${getDeviceKey()}:smokeAlarm skipping."
2559 | }
2560 |
2561 | // Add Arm Stay switch/indicator combo if it does not exist.
2562 | addAD2VirtualDevices("armStay", "Stay", false, true, true)
2563 |
2564 | // Add Arm Away switch/indicator combo if it does not exist.
2565 | addAD2VirtualDevices("armAway", "Away", false, true, true)
2566 |
2567 | // Add Exit switch/indicator combo if it does not exist.
2568 | addAD2VirtualDevices("exit", "Exit", false, true, true)
2569 |
2570 | // Add Chime Mode toggle switch/indicator combo if does not exist.
2571 | addAD2VirtualDevices("chimeMode", "Chime", false, true, true)
2572 |
2573 | // Add Bypass status contact if it does not exist.
2574 | addAD2VirtualDevices("bypass", "Bypass", false, false, true)
2575 |
2576 | // Add Ready status contact if it does not exist.
2577 | addAD2VirtualDevices("ready", "Ready", false, false, true)
2578 |
2579 | // Add perimeter only status contact if it does not exist.
2580 | addAD2VirtualDevices("perimeterOnly",
2581 | "Perimeter Only", false, false, true)
2582 |
2583 | // Add entry delay off status contact if it does not exist.
2584 | addAD2VirtualDevices("entryDelayOff",
2585 | "Entry Delay Off", false, false, true)
2586 |
2587 | // Add virtual Alarm Bell switch/indicator combo if does not exist.
2588 | addAD2VirtualDevices("alarmBell",
2589 | "Alarm Bell", false, true, true)
2590 |
2591 | // Add FIRE Alarm switch/indicator combo if it does not exist.
2592 | addAD2VirtualDevices("alarmFire", "Fire Alarm", false, true, true)
2593 |
2594 | // Add Panic Alarm switch/indicator combo if it does not exist.
2595 | addAD2VirtualDevices("alarmPanic",
2596 | "Panic Alarm", false, true, false)
2597 |
2598 | // Add AUX Alarm switch/indicator combo if it does not exist.
2599 | addAD2VirtualDevices("alarmAUX", "AUX Alarm", false, true, false)
2600 |
2601 | // Add Disarm button if it does not exist.
2602 | if (CREATE_DISARM) {
2603 | addAD2VirtualDevices("disarm", "Disarm", false, true, false)
2604 | }
2605 | } else {
2606 | log.warn "addExistingDevices: NOCREATEDEV enabled skip device creation."
2607 | }
2608 |
2609 | }
2610 | }
2611 |
2612 |
2613 | /**
2614 | * Add Virtual button and contact
2615 | */
2616 | def addAD2VirtualDevices(name, label, initstate, createButton, createContact) {
2617 |
2618 | if (createButton) {
2619 | // Add switch/indicator combo if it does not exist.
2620 | def cd = \
2621 | getChildDevice("${getDeviceKey()}:${name}")
2622 |
2623 | if (!cd) {
2624 | try {
2625 | def nd = \
2626 | addChildDevice(
2627 | APPNAMESPACE,
2628 | "AlarmDecoder action button indicator",
2629 | "${getDeviceKey()}:${name}",
2630 | state.hubId,
2631 | [
2632 | name: "${getDeviceKey()}:${name}",
2633 | label: "${sname} ${label}",
2634 | completedSetup: true
2635 | ]
2636 | )
2637 | nd.sendEvent(
2638 | name: "switch",
2639 | value: (initstate ? "on" : "off"),
2640 | isStateChange: true,
2641 | displayed: false
2642 | )
2643 | } catch (e) {
2644 | log.info "Error creating device: ${name}"
2645 | }
2646 | } else {
2647 | log.warn "addAD2VirtualDevices: Already found device " +
2648 | "${getDeviceKey()}:${name} skipping."
2649 | return
2650 | }
2651 | }
2652 |
2653 | if (createContact) {
2654 | // Add contact status contact if it does not exit.
2655 | def cd = \
2656 | getChildDevice("${getDeviceKey()}:${name}Status")
2657 |
2658 | if (!cd) {
2659 | try {
2660 | def nd = \
2661 | addChildDevice(
2662 | APPNAMESPACE,
2663 | "AlarmDecoder status indicator",
2664 | "${getDeviceKey()}:${name}Status",
2665 | state.hubId,
2666 | [
2667 | name: "${getDeviceKey()}:${name}Status",
2668 | label: "${sname} ${label} Status",
2669 | completedSetup: true
2670 | ]
2671 | )
2672 | nd.sendEvent(
2673 | name: "contact",
2674 | value: (initstate ? "closed" : "open"),
2675 | isStateChange: true,
2676 | displayed: false
2677 | )
2678 | } catch (e) {
2679 | log.info "Error creating device: ${getDeviceKey()}:${name}Status"
2680 | }
2681 | } else {
2682 | log.warn "addAD2VirtualDevices: Already found device " +
2683 | "${getDeviceKey()}:${name}Status skipping."
2684 | return
2685 | }
2686 | }
2687 | }
2688 |
2689 | /**
2690 | * Configure subscriptions the virtual devices will send too.
2691 | */
2692 | private def configureDeviceSubscriptions() {
2693 | if (debug) log.debug("configureDeviceSubscriptions")
2694 | def device = getChildDevice("${getDeviceKey()}")
2695 | if (!device) {
2696 | log.error("configureDeviceSubscriptions: Could not find primary" +
2697 | " device for '${getDeviceKey()}'.")
2698 | return
2699 | }
2700 |
2701 | /* Handle events sent from the AlarmDecoder network appliance device
2702 | * to update virtual zones when they change.
2703 | */
2704 | subscribe(device, "zone-on", zoneOn, [filterEvents: false])
2705 | subscribe(device, "zone-off", zoneOff, [filterEvents: false])
2706 |
2707 | // Subscribe to our own alarm status events from our primary device
2708 | subscribe(device, "alarmStatus", alarmdecoderAlarmHandler,
2709 | [filterEvents: false])
2710 |
2711 | // subscrib to smoke-set handler for updates
2712 | subscribe(device, "smoke-set", smokeSet, [filterEvents: false])
2713 |
2714 | // subscribe to arm-away handler
2715 | subscribe(device, "arm-away-set", armAwaySet, [filterEvents: false])
2716 |
2717 | // subscribe to arm-stay handler
2718 | subscribe(device, "arm-stay-set", armStaySet, [filterEvents: false])
2719 |
2720 | // subscribe to chime handler
2721 | subscribe(device, "chime-set", chimeSet, [filterEvents: false])
2722 |
2723 | // subscribe to exit handler
2724 | subscribe(device, "exit-set", exitSet, [filterEvents: false])
2725 |
2726 | // subscribe to perimeter-only-set handler
2727 | subscribe(device, "perimeter-only-set", perimeterOnlySet,
2728 | [filterEvents: false])
2729 |
2730 | // subscribe to entry-deley-off-set handler
2731 | subscribe(device, "entry-delay-off-set", entryDelayOffSet,
2732 | [filterEvents: false])
2733 |
2734 | // subscribe to bypass handler
2735 | subscribe(device, "bypass-set", bypassSet, [filterEvents: false])
2736 |
2737 | // subscribe to alarm bell handler
2738 | subscribe(device, "alarmbell-set", alarmBellSet, [filterEvents: false])
2739 |
2740 | // subscribe to ready handler
2741 | subscribe(device, "ready-set", readySet, [filterEvents: false])
2742 |
2743 | // subscribe to disarm handler
2744 | subscribe(device, "disarm-set", disarmSet, [filterEvents: false])
2745 |
2746 | // subscribe to CID handler
2747 | subscribe(device, "cid-set", cidSet, [filterEvents: false])
2748 |
2749 | // subscribe to RFX handler
2750 | subscribe(device, "rfx-set", rfxSet, [filterEvents: false])
2751 |
2752 | }
2753 |
2754 | /**
2755 | * Parse local network messages to a parsedEvent object.
2756 | *
2757 | * May be to UDP port 1900 for UPNP message or to TCP port 39500
2758 | * for local network to hub push messages.
2759 | *
2760 | * parsedEvent structure
2761 | * [
2762 | * devicetype:??,
2763 | * mac:XXXXXXXXXX02,
2764 | * networkAddress:??,
2765 | * deviceAddress:??,
2766 | * ssdpPath: "",
2767 | * ssdpUSN: "",
2768 | * ssdpTerm: "",
2769 | * headers: "The raw headers already base64 decoded",
2770 | * contenttype: "The parsed content type",
2771 | * body: "The raw body already base64 decoded."
2772 | * ]
2773 | *
2774 | */
2775 | private def parseEventMessage(String message) {
2776 |
2777 | if (debug)
2778 | log.debug "parseEventMessage: $message"
2779 |
2780 | def event = [: ]
2781 | try {
2782 | def parts = message.split(',')
2783 | parts.each {
2784 | part->
2785 | part = part.trim()
2786 | if (part.startsWith('devicetype:')) {
2787 | def valueString = part.split(":")[1].trim()
2788 | event.devicetype = valueString
2789 | } else if (part.startsWith('mac:')) {
2790 | def valueString = part.split(":")[1].trim()
2791 | if (valueString) {
2792 | event.mac = valueString
2793 | }
2794 | } else if (part.startsWith('requestId:')) {
2795 | // If we made the request we will get the requestId of the host we
2796 | // contacted. If we did not provide one in HubAction() then it will be
2797 | // auto generated ex. c089d06f-ba3c-4baa-a1a4-950b9ffd372a
2798 | part -= "requestId:"
2799 | def valueString = part.trim()
2800 | if (valueString) {
2801 | event.requestId = valueString
2802 | }
2803 | } else if (part.startsWith('ip:')) {
2804 | // If we made the request we will get the IP of the host we contacted.
2805 | part -= "ip:"
2806 | def valueString = part.trim()
2807 | if (valueString) {
2808 | event.ip = valueString
2809 | }
2810 | } else if (part.startsWith('port:')) {
2811 | // If we made the request we will get the PORT of the host we contacted.
2812 | part -= "port:"
2813 | def valueString = part.trim()
2814 | if (valueString) {
2815 | event.port = valueString
2816 | }
2817 | } else if (part.startsWith('networkAddress:')) {
2818 | def valueString = part.split(":")[1].trim()
2819 | if (valueString) {
2820 | event.ip = valueString
2821 | }
2822 | } else if (part.startsWith('deviceAddress:')) {
2823 | def valueString = part.split(":")[1].trim()
2824 | if (valueString) {
2825 | event.port = valueString
2826 | }
2827 | } else if (part.startsWith('ssdpPath:')) {
2828 | part -= "ssdpPath:"
2829 | def valueString = part.trim()
2830 | if (valueString) {
2831 | event.ssdpPath = valueString
2832 | }
2833 | } else if (part.startsWith('ssdpUSN:')) {
2834 | part -= "ssdpUSN:"
2835 | def valueString = part.trim()
2836 | if (valueString) {
2837 | event.ssdpUSN = valueString
2838 | }
2839 | } else if (part.startsWith('ssdpTerm:')) {
2840 | part -= "ssdpTerm:"
2841 | def valueString = part.trim()
2842 | if (valueString) {
2843 | event.ssdpTerm = valueString
2844 | }
2845 | } else if (part.startsWith('headers:')) {
2846 | part -= "headers:"
2847 | def valueString = part.trim()
2848 | if (valueString) {
2849 | if (parse_headers) {
2850 | /*
2851 | // Testing parsing full headers
2852 | def headers = [:]
2853 | def str = new String(valueString.decodeBase64())
2854 | str.eachLine { line, lineNumber ->
2855 | if (lineNumber == 0) {
2856 | headers.status = line
2857 | return
2858 | }
2859 | headers << stringToMap(line)
2860 | }
2861 | event.headers = headers
2862 | */
2863 | } else {
2864 | // decode the headers.
2865 | event.headers = new String(valueString.decodeBase64())
2866 | // extract the content type.
2867 | event.contenttype =
2868 | (event.headers =~ /Content-Type:.*/) ?
2869 | (event.headers =~ /Content-Type:.*/)[0] : null
2870 | }
2871 | }
2872 | } else if (part.startsWith('body:')) {
2873 | part -= "body:"
2874 | def valueString = part.trim()
2875 | if (valueString) {
2876 | event.body = new String(valueString.decodeBase64())
2877 | }
2878 | }
2879 | }
2880 | } catch (Exception e) {
2881 | log.error("exception ${e} in parseEventMessage parsing: ${message}")
2882 | }
2883 |
2884 | // return the parsedEvent
2885 | return event
2886 | }
2887 |
2888 | /**
2889 | * Send a request for the description.xml For every known AlarmDecoder
2890 | * we have discovered that is not verified.
2891 | */
2892 | def verifyAlarmDecoders() {
2893 | def devices = getDevices().findAll {
2894 | it?.value?.verified != true
2895 | }
2896 |
2897 | if (devices) {
2898 | log.warn "verifyAlarmDecoders: UNVERIFIED Decoders!: $devices"
2899 | }
2900 |
2901 | devices.each {
2902 | if (it?.value?.ssdpPath?.contains("xml")) {
2903 | verifyAlarmDecoder(
2904 | (it?.value?.ip + ":" + it?.value?.port),
2905 | it?.value?.ssdpPath
2906 | )
2907 | } else {
2908 | log.warn("verifyAlarmDecoders: invalid ssdpPath not an xml file")
2909 | }
2910 | }
2911 | }
2912 |
2913 | /**
2914 | * Send a GET request from HUB to the AlarmDecoder for its descrption.xml file
2915 | */
2916 | def verifyAlarmDecoder(String DNI, String ssdpPath) {
2917 | sendVerify(DNI, ssdpPath)
2918 | }
2919 |
2920 | /**
2921 | * Convert from internal format networkAddress:C0A8016F to a real IP address
2922 | * string ex. 192.168.1.111
2923 | */
2924 | private String convertHexToIP(hex) {
2925 | [convertHexToInt(hex[0..1]),
2926 | convertHexToInt(hex[2..3]),
2927 | convertHexToInt(hex[4..5]),
2928 | convertHexToInt(hex[6..7])
2929 | ].join(".")
2930 | }
2931 |
2932 | /**
2933 | * Convert from ip to internal format C0A8016F
2934 | * FIXME: Needs more groovy.
2935 | */
2936 | private String convertIPToHex(ip) {
2937 | def parts = ip.split(".")
2938 | parts[0] = Integer.toHexString(parts[0])
2939 | parts[1] = Integer.toHexString(parts[1])
2940 | parts[2] = Integer.toHexString(parts[2])
2941 | parts[3] = Integer.toHexString(parts[3])
2942 | return parts.join("")
2943 | }
2944 |
2945 | /**
2946 | * convert hex encoded string to integer
2947 | */
2948 | private Integer convertHexToInt(hex) {
2949 | Integer.parseInt(hex, 16)
2950 | }
2951 |
2952 | /**
2953 | * return a device key appropriate for the platform
2954 | */
2955 | private String getDeviceKey() {
2956 | def key = ""
2957 | if (isSmartThings())
2958 | key = "${state.ip}:${state.port}"
2959 | else if (isHubitat())
2960 | key = "${state.ip}"
2961 |
2962 | return key
2963 | }
2964 |
2965 | private String getDeviceKey(ip, port) {
2966 | def key = ""
2967 | if (isSmartThings())
2968 | key = "${ip}:${port}"
2969 | else if (isHubitat())
2970 | key = "${ip}"
2971 |
2972 | return key
2973 | }
2974 |
2975 | /**
2976 | * return this hubs URN:XXX.XXX.XXX.XXX:YYYY
2977 | * X = IP
2978 | * Y = Port
2979 | */
2980 | private String getHubURN() {
2981 | def urn = null
2982 | if (isSmartThings()) {
2983 | def hub = location.hubs[0]
2984 | def ip = hub.localIP
2985 | def port = hub.localSrvPortTCP
2986 | urn = "${ip}:${port}"
2987 | } else if (isHubitat()) {
2988 | def hub = location.hubs[0]
2989 | def ip = hub.getDataValue("localIP")
2990 | def port = hub.getDataValue("localSrvPortTCP")
2991 | urn = "${ip}:${port}"
2992 | }
2993 | return urn
2994 | }
2995 |
2996 | /**
2997 | * build a URI host address of the AlarmDecoder web appliance for web requests.
2998 | * ex. AABBCCDD:XXXX -> 192.168.1.1:5000
2999 | */
3000 | private getHostAddressFromDNI(d) {
3001 | def ip = ""
3002 | def port = ""
3003 | if (d) {
3004 | def parts = d.split(":")
3005 | if (parts.size() == 2) {
3006 | ip = convertHexToIP(parts[0])
3007 | port = convertHexToInt(parts[1])
3008 | }
3009 | }
3010 | return ip + ":" + port
3011 | }
3012 |
3013 | /**
3014 | * return a device network name rightmost data Z
3015 | * SmartThings AABBCCDD:XXXX:ZZZZZZZZZ
3016 | * Hubitat AABBCCDD:ZZZZZZZZZ
3017 | */
3018 | private getDeviceNamePart(d) {
3019 | def result = ""
3020 | if (isSmartThings()) {
3021 | result = d.deviceNetworkId.split(":")[2].trim()
3022 | } else if (isHubitat()) {
3023 | result = d.deviceNetworkId.split(":")[1].trim()
3024 | }
3025 | return result
3026 | }
3027 |
3028 | /**
3029 | * Send a state change to an AD2 virtual device adjusting it to
3030 | * the devices actual capabilities and inverting if preferred.
3031 | *
3032 | * ad2d: The device handle.
3033 | * state: [on, off]
3034 | * Capabilities: [Switch]
3035 | * Default off = off, on(Alerting) = on
3036 | * Capabilities: [Contact Sensor]
3037 | * Default close = off, open(Alerting) = on
3038 | * Capabilities: [Smoke Detector]
3039 | * Default clear = off, detected(Alerting) = on
3040 | *
3041 | */
3042 | def _sendEventTranslate(ad2d, state) {
3043 |
3044 | // Grab the devices preferences for inverting
3045 | def invert = (ad2d.device.getDataValue("invert") == "true" ? true : false)
3046 |
3047 | // send a switch event if its a [Switch]
3048 | // Default off = Off, on(Alerting) = On
3049 | if (ad2d.hasCapability("Switch")) {
3050 | // convert: Any matches is ON(true) no match is OFF(false)
3051 | def sval = ((state == "on") ? true : false)
3052 |
3053 | // invert: If device has invert attribute then invert signal
3054 | sval = (invert ? !sval : sval)
3055 |
3056 | // send 'switch' event
3057 | ad2d.sendEvent(
3058 | name: "switch",
3059 | value: (sval ? "on" : "off"),
3060 | isStateChange: true,
3061 | filtered: true
3062 | )
3063 | }
3064 |
3065 | // send a 'contact' event if its a [Contact Sensor]
3066 | // Default close = Off, open(Alerting) = On
3067 | if (ad2d.hasCapability("Contact Sensor")) {
3068 | // convert: Any matches is ON(true) no match is OFF(false)
3069 | def sval = ((state == "on") ? true : false)
3070 |
3071 | // invert: If device has invert attribute then invert signal
3072 | sval = (invert ? !sval : sval)
3073 |
3074 | // send switch event
3075 | ad2d.sendEvent(
3076 | name: "contact",
3077 | value: (sval ? "open" : "closed"),
3078 | isStateChange: true,
3079 | filtered: true
3080 | )
3081 | }
3082 |
3083 | // send a 'motion' event if its a [Motion Sensor]
3084 | // Default inactive = Off, active(Alerting) = On
3085 | if (ad2d.hasCapability("Motion Sensor")) {
3086 | // convert: Any matches is ON(true) no match is OFF(false)
3087 | def sval = ((state == "on") ? true : false)
3088 |
3089 | // invert: If device has invert attribute then invert signal
3090 | sval = (invert ? !sval : sval)
3091 |
3092 | // send switch event
3093 | ad2d.sendEvent(
3094 | name: "motion",
3095 | value: (sval ? "active" : "inactive"),
3096 | isStateChange: true,
3097 | filtered: true
3098 | )
3099 | }
3100 |
3101 | // send a 'shock' event if its a [Shock Sensor]
3102 | // Default clear = Off, detected(Alerting) = On
3103 | if (ad2d.hasCapability("Shock Sensor")) {
3104 | // convert: Any matches is ON(true) no match is OFF(false)
3105 | def sval = ((state == "on") ? true : false)
3106 |
3107 | // invert: If device has invert attribute then invert signal
3108 | sval = (invert ? !sval : sval)
3109 |
3110 | // send switch event
3111 | ad2d.sendEvent(
3112 | name: "shock",
3113 | value: (sval ? "detected" : "clear"),
3114 | isStateChange: true,
3115 | filtered: true
3116 | )
3117 | }
3118 |
3119 | // send a 'carbonMonoxide' event if its a [Carbon Monoxide Detector]
3120 | // Default clear = Off, detected(Alerting) = On
3121 | if (ad2d.hasCapability("Carbon Monoxide Detector")) {
3122 | // convert: Any matches is ON(true) no match is OFF(false)
3123 | def sval = ((state == "on") ? true : false)
3124 |
3125 | // invert: If device has invert attribute then invert signal
3126 | sval = (invert ? !sval : sval)
3127 |
3128 | // send switch event
3129 | ad2d.sendEvent(
3130 | name: "carbonMonoxide",
3131 | value: (sval ? "detected" : "clear"),
3132 | isStateChange: true,
3133 | filtered: true
3134 | )
3135 | }
3136 |
3137 | // send a 'smoke' event if its a [Smoke Detector]
3138 | // Default clear = Off, detected(Alerting) = On
3139 | if (ad2d.hasCapability("Smoke Detector")) {
3140 | // convert: Any matches is ON(true) no match is OFF(false)
3141 | def sval = ((state == "on") ? true : false)
3142 |
3143 | // invert: If device has invert attribute then invert signal
3144 | sval = (invert ? !sval : sval)
3145 |
3146 | // send switch event
3147 | ad2d.sendEvent(
3148 | name: "smoke",
3149 | value: (sval ? "detected" : "clear"),
3150 | isStateChange: true,
3151 | filtered: true
3152 | )
3153 | }
3154 | }
3155 |
--------------------------------------------------------------------------------