├── 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 | --------------------------------------------------------------------------------