├── adt-pulse-mqtt ├── build.json ├── .dockerignore ├── armhf │ ├── resin-xbuild │ └── qemu-arm-static ├── run.sh ├── Dockerfile ├── Dockerfile-amd64 ├── package.json ├── Dockerfile-armhf ├── config.json ├── server.js └── adt-pulse.js ├── repository.json ├── docker-hub-build.sh ├── docker-build.sh ├── LICENSE ├── .gitignore ├── devicetypes └── haruny │ └── VirtualADTAlarmSystem.src │ ├── ADTVirtualOpenClose.groovy │ ├── ADTVirtualMotion.groovy │ └── VirtualADTAlarmSystem.groovy ├── README.md └── smartapps └── haruny └── ADTAlarmSmartApp.src └── ADTAlarmSmartApp.groovy /adt-pulse-mqtt/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "squash": false 3 | } 4 | -------------------------------------------------------------------------------- /adt-pulse-mqtt/.dockerignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | package-lock.json -------------------------------------------------------------------------------- /adt-pulse-mqtt/armhf/resin-xbuild: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haruny/adt-pulse-mqtt/HEAD/adt-pulse-mqtt/armhf/resin-xbuild -------------------------------------------------------------------------------- /adt-pulse-mqtt/armhf/qemu-arm-static: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haruny/adt-pulse-mqtt/HEAD/adt-pulse-mqtt/armhf/qemu-arm-static -------------------------------------------------------------------------------- /adt-pulse-mqtt/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | CONFIG_PATH=/data/options.json 5 | 6 | # start server 7 | npm start 8 | 9 | -------------------------------------------------------------------------------- /repository.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ADT Pulse Bridge using MQTT", 3 | "url": "https://github.com/haruny/adt-pulse-mqtt", 4 | "maintainer": "Harun Yayli" 5 | } 6 | -------------------------------------------------------------------------------- /docker-hub-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd adt-pulse-mqtt 4 | 5 | # amd64 build is automated on Docker Hub from Github Repo 6 | # docker build -f "Dockerfile-amd64" -t digitalcraig/adt-pulse-mqtt:amd64-latest . 7 | 8 | # armhf is failing cross-build on Docker Hub, so build locally and push 9 | docker build -f "Dockerfile-armhf" -t digitalcraig/adt-pulse-mqtt:armhf-latest . 10 | docker push digitalcraig/adt-pulse-mqtt:armhf-latest 11 | -------------------------------------------------------------------------------- /docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd adt-pulse-mqtt 4 | 5 | # Default build is based on homeassistant/amd64-base:latest 6 | #docker build -t local/adt-pulse-mqtt . 7 | 8 | # To specify a different platform, set build-arg to a different base image: 9 | # homeassistant/armhf-base 10 | # homeassistant/amd64-base 11 | # homeassistant/aarch64-base 12 | # homeassistant/i386-base 13 | 14 | docker build --build-arg BUILD_FROM="homeassistant/amd64-base:latest" -t local/adt-pulse-mqtt . 15 | 16 | 17 | -------------------------------------------------------------------------------- /adt-pulse-mqtt/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BUILD_FROM=homeassistant/amd64-base:latest 2 | FROM $BUILD_FROM 3 | 4 | ENV LANG C.UTF-8 5 | ENV NODE_ENV production 6 | 7 | # Install node and npm 8 | RUN apk add --update nodejs 9 | RUN apk add --update nodejs-npm 10 | 11 | WORKDIR /usr/src/app 12 | 13 | # Install app dependencies 14 | COPY package.json . 15 | RUN npm install 16 | 17 | # Bundle app source 18 | COPY . . 19 | 20 | # Copy data for add-on 21 | COPY run.sh / 22 | RUN chmod a+x /run.sh 23 | 24 | CMD [ "/run.sh" ] 25 | -------------------------------------------------------------------------------- /adt-pulse-mqtt/Dockerfile-amd64: -------------------------------------------------------------------------------- 1 | ARG BUILD_FROM=homeassistant/amd64-base:latest 2 | FROM $BUILD_FROM 3 | 4 | ENV LANG C.UTF-8 5 | ENV NODE_ENV production 6 | 7 | # Install node and npm 8 | RUN apk add --update nodejs 9 | RUN apk add --update nodejs-npm 10 | 11 | WORKDIR /usr/src/app 12 | 13 | # Install app dependencies 14 | COPY package.json . 15 | RUN npm install 16 | 17 | # Bundle app source 18 | COPY . . 19 | 20 | # Copy data for add-on 21 | COPY run.sh / 22 | RUN chmod a+x /run.sh 23 | 24 | CMD [ "/run.sh" ] -------------------------------------------------------------------------------- /adt-pulse-mqtt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pulse-adt-mqtt", 3 | "version": "2.3.0", 4 | "description": "Home Assistant ADT Pulse Bridge using MQTT", 5 | "author": "Harun Yayli", 6 | "main": "server.js", 7 | "scripts": { 8 | "start": "node server.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/haruny/adt-pulse-mqtt" 13 | }, 14 | "license": "ISC", 15 | "dependencies": { 16 | "mqtt": "^2.13.0", 17 | "cheerio": "0.22.0", 18 | "q": "~1.0.1", 19 | "request": "2.88.2", 20 | "tough-cookie": "^2.3.0", 21 | "lodash": "^4.17.15" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /adt-pulse-mqtt/Dockerfile-armhf: -------------------------------------------------------------------------------- 1 | ARG BUILD_FROM=arm32v6/alpine:3.6 2 | FROM $BUILD_FROM 3 | 4 | ENV LANG C.UTF-8 5 | ENV NODE_ENV production 6 | ENV QEMU_EXECVE 1 7 | 8 | COPY armhf/qemu-arm-static /usr/bin 9 | COPY armhf/resin-xbuild /usr/bin 10 | 11 | RUN [ "/usr/bin/qemu-arm-static", "/bin/sh", "-c", "ln -s /usr/bin/resin-xbuild /usr/bin/cross-build-start; ln -s /usr/bin/resin-xbuild /usr/bin/cross-build-end; ln /bin/sh /bin/sh.real" ] 12 | 13 | RUN [ "cross-build-start" ] 14 | 15 | # Install node and npm 16 | RUN apk add --update nodejs 17 | RUN apk add --update nodejs-npm 18 | 19 | WORKDIR /usr/src/app 20 | 21 | # Install app dependencies 22 | COPY package.json . 23 | RUN npm install 24 | 25 | # Bundle app source 26 | COPY . . 27 | 28 | # Copy data for add-on 29 | COPY run.sh / 30 | RUN chmod a+x /run.sh 31 | 32 | RUN [ "cross-build-end" ] 33 | 34 | CMD [ "/run.sh" ] 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Craig Leikis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | adt-pulse-mqtt/package-lock.json 61 | package-lock.json 62 | -------------------------------------------------------------------------------- /adt-pulse-mqtt/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ADT Pulse MQTT", 3 | "version": "2.3.0", 4 | "slug": "adtpulsemqtt", 5 | "description": "ADT Pulse Bridge using MQTT", 6 | "url":"https://github.com/haruny/adt-pulse-mqtt", 7 | "startup": "application", 8 | "boot": "auto", 9 | "host_network" : false, 10 | "arch": [ 11 | "armhf", 12 | "amd64" 13 | ], 14 | "map": [ 15 | "share:rw", 16 | "ssl" 17 | ], 18 | "options": { 19 | "ssl": false, 20 | "certfile": "fullchain.pem", 21 | "keyfile": "privkey.pem", 22 | "pulse_login" : { 23 | "username": "", 24 | "password": "" 25 | }, 26 | "mqtt_host" : "core-mosquitto", 27 | "mqtt_url" : "", 28 | "mqtt_connect_options" : { 29 | "username" : "", 30 | "password" : "" 31 | }, 32 | "alarm_state_topic": "home/alarm/state", 33 | "alarm_command_topic": "home/alarm/cmd", 34 | "zone_state_topic": "adt/zone", 35 | "smartthings_topic": "smartthings", 36 | "smartthings": false 37 | }, 38 | "schema": { 39 | "ssl": "bool", 40 | "certfile": "str", 41 | "keyfile": "str", 42 | "pulse_login" : { 43 | "username": "str", 44 | "password": "str" 45 | }, 46 | "mqtt_host" : "str", 47 | "mqtt_url" : "str?", 48 | "mqtt_connect_options" : { 49 | "username" : "str", 50 | "password" : "str" 51 | }, 52 | "alarm_state_topic" : "str", 53 | "alarm_command_topic": "str", 54 | "zone_state_topic": "str", 55 | "smartthings_topic": "str", 56 | "smartthings": "bool" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /devicetypes/haruny/VirtualADTAlarmSystem.src/ADTVirtualOpenClose.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Virtual ADT Open/Closed Sensor 3 | * 4 | * Copyright 2018 Harun Yayli 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | 17 | 18 | metadata { 19 | definition(name: "Virtual ADT Open/Closed Sensor", namespace: "haruny", author: "Harun Yayli") { 20 | capability "Contact Sensor" 21 | capability "doorControl" 22 | 23 | } 24 | 25 | simulator { 26 | status "open": "open" 27 | status "closed": "closed" 28 | 29 | } 30 | 31 | tiles(scale: 2) { 32 | multiAttributeTile(name: "contact", type: "generic", width: 6, height: 4) { 33 | tileAttribute("device.contact", key: "PRIMARY_CONTROL") { 34 | attributeState "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#e86d13" 35 | attributeState "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#00A0DC" 36 | } 37 | } 38 | 39 | main("contact") 40 | details("contact") 41 | } 42 | } 43 | 44 | def parse(String description) { 45 | log.debug "description: $description" 46 | 47 | if (description.startsWith("open")){ 48 | return createEvent(name:"contact", value:"open") 49 | } 50 | 51 | if (description.startsWith("closed")){ 52 | return createEvent(name:"contact", value:"closed") 53 | } 54 | 55 | return result 56 | } 57 | 58 | def open(){ 59 | parse("open") 60 | sendEvent(name:"contact", value:"open") 61 | } 62 | 63 | def close(){ 64 | parse("closed") 65 | sendEvent(name:"contact", value:"closed") 66 | } 67 | -------------------------------------------------------------------------------- /devicetypes/haruny/VirtualADTAlarmSystem.src/ADTVirtualMotion.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Virtual ADT Motion Sensor 3 | * 4 | * Copyright 2018 Harun Yayli 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 | * in compliance with the License. You may obtain a copy of the License at: 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 12 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 13 | * for the specific language governing permissions and limitations under the License. 14 | * 15 | */ 16 | 17 | 18 | metadata { 19 | definition(name: "Virtual ADT Motion Sensor", namespace: "haruny", author: "Harun Yayli") { 20 | capability "Motion Sensor" 21 | capability "Sensor" 22 | 23 | command "active" 24 | command "inactive" 25 | } 26 | 27 | simulator { 28 | status "active": "active" 29 | status "inactive": "inactive" 30 | 31 | } 32 | 33 | tiles(scale: 2) { 34 | multiAttributeTile(name:"motion", type: "generic", width: 6, height: 4){ 35 | tileAttribute("device.motion", key: "PRIMARY_CONTROL") { 36 | attributeState("active", label:'${name}', icon:"st.motion.motion.active", backgroundColor:"#00A0DC") 37 | attributeState("inactive", label:'${name}', icon:"st.motion.motion.inactive", backgroundColor:"#CCCCCC") 38 | } 39 | } 40 | main "motion" 41 | details "motion" 42 | } 43 | } 44 | 45 | def parse(String description) { 46 | log.debug "description: $description" 47 | 48 | if (description.startsWith("active")){ 49 | return createEvent(name:"motion", value:"active") 50 | } 51 | 52 | if (description.startsWith("inactive")){ 53 | return createEvent(name:"motion", value:"inactive") 54 | } 55 | 56 | return result 57 | } 58 | 59 | def active(){ 60 | parse("active") 61 | sendEvent(name:"motion", value:"active") 62 | } 63 | 64 | def inactive(){ 65 | parse("inactive") 66 | sendEvent(name:"motion", value:"inactive") 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEFUNCT! 2 | 3 | As of 1/25/2021 This repo is no longer maintained. Please see https://github.com/adt-pulse-mqtt/adt-pulse-mqtt. Update your Home Assistant add-on repsitory to https://github.com/adt-pulse-mqtt/hassio. 4 | 5 | 6 | # adt-pulse-mqtt 7 | ADT Pulse bridge for Home Assistant using MQTT. 8 | 9 | Integrates ADT Pulse to Home Assistant. You can also choose to add the ADT Pulse alarm system and ADT devices to your SmartThings. 10 | SmartApp allows automatic running our Routines upon alarm changing states. 11 | 12 | ## Hassio Setup 13 | Add the repository (https://github.com/adt-pulse-mqtt/hassio) to Hassio. 14 | Hit Install. Don't forget to configure `pulse_login` with your ADT Pulse Portal username and password. I recommend using a separate login for Home Assistant use. 15 | You'll need an MQTT broker to run this. I'm using Mosquitto broker (https://www.home-assistant.io/addons/mosquitto/). 16 | 17 | In most cases, the mqtt_host option and the mqtt username and password options are sufficient. 18 | For advanced confgurations, you may want to use the mqtt_url option instead. Additional connections options are available (see https://www.npmjs.com/package/mqtt#connect). 19 | 20 | ### Configuration 21 | Change the config of the app in hassio then edit the configuration.yaml: 22 | 23 | To add the control panel: 24 | 25 |
alarm_control_panel:
26 |   - platform: mqtt
27 |     name: "ADT Pulse"
28 |     state_topic: "home/alarm/state"
29 |     command_topic: "home/alarm/cmd"
30 |     payload_arm_home: "arm_home"
31 |     payload_arm_away: "arm_away"
32 |     payload_disarm: "disarm"
33 | 
34 | 35 | After running the add-on, to list all the zones found, you can call: 36 |
37 | # mosquitto_sub -h YOUR_MQTT_IP -v -t "adt/zone/#"
38 | 
39 | 40 | Add the following to the configuration.yaml for each zone in binary_sensor: 41 | 42 |
43 | binary_sensor:
44 |   - platform: mqtt
45 |     name: "Kitchen Door"
46 |     state_topic: "adt/zone/Kitchen Door/state"
47 |     payload_on: "devStatOpen" # Use devStatTamper for shock devices
48 |     payload_off: "devStatOK" # 
49 |     device_class: door
50 |     retain: true
51 |     value_template: '{{ value_json.status }}' 
52 | 
53 | Note: State topic names come from your Pulse configuration. 54 | 55 | The possibible state values are: 56 | 57 | * devStatOK (device okay) 58 | * devStatOpen (door/window opened) 59 | * devStatMotion (detected motion) 60 | * devStatTamper (glass broken or device tamper) 61 | * devStatAlarm (detected CO/Smoke) 62 | * devStatUnknown (device offline) 63 | 64 | I'm limited with what I have as zones, for different devices please submit your MQTT dump (for the zones) in issues. I'll try to add the support for it. 65 | 66 | 67 | ## Smartthings Support 68 | 69 | * In Hassio, setting of the ADT Pulse MQTT set 70 | 71 |
72 | "smartthings": true
73 | 
74 | 75 | * In SmartThings IDE, 76 | 77 | 1. add the following devicehandlers: 78 | https://github.com/haruny/adt-pulse-mqtt/tree/master/devicetypes/haruny/VirtualADTAlarmSystem.src 79 | 1. add the following SmartApp: 80 | https://github.com/haruny/adt-pulse-mqtt/tree/master/smartapps/haruny/ADTAlarmSmartApp.src 81 | 1. Add your devices using SmartThings IDE. You have to name them the same way they appear in ADT Portal. 82 | 1. Run the SmartApp in your mobile application. Follow the instructions. Do not rename ADT Alarm System device created by the app. Multiple alarm systems/locations is not supported. 83 | 1. In MQTT Bridge app, select all the devices created (Alarm system, contacts, motion etc.) 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /adt-pulse-mqtt/server.js: -------------------------------------------------------------------------------- 1 | const Pulse = require('./adt-pulse.js'); 2 | const mqtt = require('mqtt'); 3 | var config = require('/data/options.json'); 4 | 5 | var myAlarm = new Pulse(config.pulse_login.username, config.pulse_login.password); 6 | 7 | // Use mqtt_url option if specified, otherwise build URL using host option 8 | if (config.mqtt_url) { 9 | var client = new mqtt.connect(config.mqtt_url, config.mqtt_connect_options); 10 | } 11 | else { 12 | var client = new mqtt.connect("mqtt://"+config.mqtt_host,config.mqtt_connect_options); 13 | } 14 | 15 | var alarm_state_topic = config.alarm_state_topic; 16 | var alarm_command_topic = config.alarm_command_topic; 17 | var zone_state_topic = config.zone_state_topic; 18 | var smartthings_topic = config.smartthings_topic; 19 | var smartthings = config.smartthings; 20 | 21 | var alarm_last_state = "unknown"; 22 | var devices = {}; 23 | 24 | client.on('connect', function () { 25 | console.log("MQTT Sub to: "+alarm_command_topic); 26 | client.subscribe(alarm_command_topic) 27 | if (smartthings){ 28 | client.subscribe(smartthings_topic+"/ADT Alarm System/alarm/state") 29 | } 30 | }); 31 | 32 | client.on('message', function (topic, message) { 33 | console.log((new Date()).toLocaleString()+" Received Message:"+ topic + ":"+message); 34 | 35 | if (smartthings && topic==smartthings_topic+"/ADT Alarm System/alarm/state" && message.toString().includes("_push")){ 36 | var toState=null; 37 | 38 | switch (message.toString()){ 39 | case "off_push": 40 | toState="disarm"; 41 | break; 42 | case "stay_push": 43 | toState="arm_home"; 44 | break; 45 | case "away_push": 46 | toState="arm_away"; 47 | break; 48 | } 49 | console.log((new Date()).toLocaleString()+" Pushing alarm state to Hass:"+toState); 50 | 51 | if (toState!=null){ 52 | client.publish(alarm_command_topic, toState,{"retain":false}); 53 | } 54 | return; 55 | } 56 | if (topic!=alarm_command_topic){ 57 | return; 58 | } 59 | 60 | var msg = message.toString(); 61 | var action; 62 | var prev_state="disarmed"; 63 | 64 | if(alarm_last_state=="armed_home") prev_state = "stay"; 65 | if(alarm_last_state=="armed_away") prev_state = "away"; 66 | 67 | if (msg =="arm_home"){ 68 | action= {'newstate':'stay','prev_state':prev_state}; 69 | } 70 | else if (msg=="disarm") { 71 | action= {'newstate':'disarm','prev_state':prev_state}; 72 | } 73 | else if (msg=="arm_away") { 74 | action = {'newstate':'away','prev_state':prev_state}; 75 | } else{ // I don't know this mode #5 76 | console.log((new Date()).toLocaleString()+" Unsupportated state requested:"+msg); 77 | return; 78 | } 79 | 80 | myAlarm.setAlarmState(action); 81 | }); 82 | 83 | // Register Callbacks: 84 | myAlarm.onDeviceUpdate( 85 | function(device) { 86 | console.log("Device callback"+ JSON.stringify(device)); 87 | } 88 | ); 89 | 90 | myAlarm.onStatusUpdate( 91 | function(device) { 92 | var mqtt_state = "unknown"; 93 | var sm_alarm_value = "off"; 94 | 95 | var status = device.status.toLowerCase(); 96 | 97 | // smartthings bridge has no typical alarm device with stay|away|alarm|home status. 98 | // we'll re-use the "alarm" and map strobe|siren|both|off to stay|away|alarm|home 99 | // Sorry I'm too lazy to write my own smartthngs bridge for now. 100 | 101 | if (status.includes('disarmed')) { 102 | mqtt_state = "disarmed"; 103 | sm_alarm_value = "off"; 104 | } 105 | if (status.includes('armed stay')) { 106 | mqtt_state = "armed_home"; 107 | sm_alarm_value = "strobe"; 108 | } 109 | if (status.includes('armed away')) { 110 | mqtt_state = "armed_away"; 111 | sm_alarm_value = "siren"; 112 | } 113 | if (status.includes('alarm')) { 114 | mqtt_state = "triggered"; 115 | sm_alarm_value = "both"; 116 | } 117 | if (status.includes('arming')) { 118 | mqtt_state = "pending"; 119 | sm_alarm_value = "siren"; // temporary 120 | } 121 | 122 | if (!mqtt_state.includes(alarm_last_state) && !mqtt_state.includes('unknown')) { 123 | console.log((new Date()).toLocaleString()+": Pushing alarm state: "+mqtt_state+" to "+alarm_state_topic); 124 | client.publish(alarm_state_topic, mqtt_state,{"retain":true}); 125 | if (smartthings){ 126 | var sm_alarm_topic = smartthings_topic+"/ADT Alarm System/alarm/cmd"; 127 | console.log((new Date()).toLocaleString()+": Pushing alarm state to smartthings"+sm_alarm_topic); 128 | client.publish(sm_alarm_topic, sm_alarm_value,{"retain":false}); 129 | } 130 | alarm_last_state = mqtt_state; 131 | } 132 | } 133 | ); 134 | 135 | myAlarm.onZoneUpdate( 136 | function(device) { 137 | 138 | var dev_zone_state_topic = zone_state_topic+"/"+device.name+"/state"; 139 | //var devValue = JSON.stringify(device); 140 | var sm_dev_zone_state_topic; 141 | 142 | // smartthings bridge assumes actionable devices have a topic set with cmd 143 | // adt/zone/DEVICE_NAME/state needs to turn into 144 | // smartthings/DEVICE_NAME/door/cmd 145 | // or 146 | // smartthings/DEVICE_NAME/motion/cmd 147 | 148 | if (smartthings){ 149 | var contactType = "door"; 150 | var contactValue= (device.state == "devStatOK")? "closed":"open"; 151 | 152 | if (device.tags.includes("motion")) { 153 | contactType="motion"; 154 | contactValue = (device.state == "devStatOK")? "inactive":"active"; 155 | } 156 | sm_dev_zone_state_topic=smartthings_topic+"/"+device.name+"/"+contactType+"/cmd"; 157 | } 158 | 159 | if (devices[device.id]==null || device.timestamp > devices[device.id].timestamp) { 160 | client.publish(dev_zone_state_topic, device.state, {"retain":false}); 161 | console.log((new Date()).toLocaleString()+": Pushing device state: " + device.state+ " to topic " + dev_zone_state_topic); 162 | 163 | if (smartthings){ 164 | client.publish(sm_dev_zone_state_topic, contactValue, {"retain":false}); 165 | console.log((new Date()).toLocaleString()+": Pushing to smartthings: "+sm_dev_zone_state_topic+" to "+contactValue); 166 | } 167 | } 168 | devices[device.id] = device; 169 | } 170 | ); 171 | 172 | 173 | myAlarm.pulse(); 174 | -------------------------------------------------------------------------------- /smartapps/haruny/ADTAlarmSmartApp.src/ADTAlarmSmartApp.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * ADT Alarm SmartApp 3 | * 4 | * v.0.0.3 5 | * Copyright 2018 HARUN YAYLI 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at: 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 13 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 14 | * for the specific language governing permissions and limitations under the License. 15 | * 16 | */ 17 | definition( 18 | name: "ADT Alarm SmartApp", 19 | namespace: "haruny", 20 | author: "Harun Yayli", 21 | description: "The app creates a virtual ADT alarm panel and allows you to run routines depending on the alarm status.\r\nTo be used in junction with ADT MQTT Bridge.\r\nhttps://github.com/haruny/adt-pulse-mqtt", 22 | category: "Safety & Security", 23 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 24 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", 25 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") 26 | 27 | 28 | preferences { 29 | page(name: "selectActions") 30 | } 31 | 32 | def selectActions() { 33 | dynamicPage(name: "selectActions", title: "Select what happens when the alarm turns into the specified state", install: true, uninstall: true) { 34 | def actions = location.helloHome?.getPhrases()*.label 35 | 36 | section("Select Hub"){ 37 | input "theHub", "hub", title: "Select the hub for the alarm device to be installed ", multiple: false, required: true 38 | } 39 | if (actions) { 40 | actions.sort() 41 | 42 | section("Disarm Actions") { 43 | input "actionDisarm", "enum", title: "Select a routine to execute when disarm:", options: actions, required: false 44 | } 45 | section("Disarm Trigger Routine") { 46 | input "triggerDisarm", "enum", title: "Select a routine to disarm the alarm after its triggered:", options: actions, required: false 47 | } 48 | section("Stay Actions") { 49 | input "actionStay", "enum", title: "Select a routine to execute when armed as Stay:", options: actions, required: false 50 | } 51 | section("Stay Trigger Routine") { 52 | input "triggerStay", "enum", title: "Select a routine to arm the alarm as stay after its triggered:", options: actions, required: false 53 | } 54 | section("Away Actions") { 55 | input "actionAway", "enum", title: "Select a routine to execute when armed as Away:", options: actions, required: false 56 | } 57 | 58 | section("Away Trigger Routine") { 59 | input "triggerAway", "enum", title: "Select a routine to arm the alarm as Away after its triggered:", options: actions, required: false 60 | } 61 | section("Alarm Actions") { 62 | input "actionAlarm", "enum", title: "Select a routine to execute when alarm is triggered", options: actions, required: false 63 | } 64 | } 65 | else{ 66 | section("No Routines found!"){ 67 | paragraph "No Routines found. Create routines and try again." 68 | } 69 | } 70 | 71 | section("Finally"){ 72 | paragraph "Don't forget to select the alarm device in the MQTT Bridge app. When you remove this app, Virtual ADT device will be removed. Before removing the app, remove the device from MQTT Bridge" 73 | } 74 | 75 | } 76 | } 77 | 78 | def installed() { 79 | log.debug "Installed with settings: ${settings}" 80 | if (getAllChildDevices().size() == 0) { 81 | def adtDevice = addChildDevice("haruny", "Virtual ADT Alarm System", "adtvas", theHub.id, [completedSetup: true, label: "ADT Alarm System"]) 82 | } 83 | initialize() 84 | } 85 | 86 | def routineChanged(evt) { 87 | 88 | if (settings.triggerStay!=null && settings.triggerStay==evt.displayName){ 89 | log.debug "ADT Alarm App caught an evt: ${evt.displayName} will push Stay button" 90 | getChildDevice("adtvas").stay_push(); 91 | } 92 | 93 | if (settings.triggerDisarm!=null && settings.triggerDisarm==evt.displayName){ 94 | log.debug "ADT Alarm App caught an evt: ${evt.displayName} will push Disarm button" 95 | getChildDevice("adtvas").off_push(); 96 | } 97 | 98 | if (settings.triggerAway!=null && settings.triggerAway==evt.displayName){ 99 | log.debug "ADT Alarm App caught an evt: ${evt.displayName} will push Away button" 100 | getChildDevice("adtvas").away_push(); 101 | } 102 | 103 | 104 | } 105 | 106 | def uninstalled() { 107 | getAllChildDevices().each { 108 | deleteChildDevice(it.deviceNetworkId) 109 | } 110 | } 111 | 112 | def updated() { 113 | log.debug "Updated with settings: ${settings}" 114 | 115 | unsubscribe() 116 | initialize() 117 | } 118 | 119 | def adtEventHandlerMethod(evt){ 120 | log.debug "Alarm had an event ${evt.value}" 121 | 122 | switch (evt.value){ 123 | case "off": 124 | if (settings.actionDisarm!=null){ 125 | log.debug "Calling debug routine ${settings.actionDisarm}" 126 | location.helloHome?.execute(settings.actionDisarm) 127 | } 128 | break; 129 | case "stay": 130 | if (settings.actionStay!=null){ 131 | log.debug "Calling debug routine ${settings.actionStay}" 132 | location.helloHome?.execute(settings.actionStay) 133 | } 134 | break; 135 | case "away": 136 | if (settings.actionAway!=null){ 137 | log.debug "Calling debug routine ${settings.actionAway}" 138 | location.helloHome?.execute(settings.actionAway) 139 | } 140 | break; 141 | case "triggered": 142 | if (settings.actionAlarm!=null){ 143 | log.debug "Calling debug routine ${settings.actionAlarm}" 144 | location.helloHome?.execute(settings.actionAlarm) 145 | } 146 | break; 147 | default: 148 | log.debug "Unknown event ${evt.value}" 149 | break; 150 | 151 | } 152 | 153 | } 154 | def initialize() { 155 | if (getAllChildDevices().size() != 0) { 156 | getAllChildDevices().each{ 157 | subscribe(it, "alarm", "adtEventHandlerMethod") 158 | } 159 | } 160 | subscribe(location, "routineExecuted", routineChanged) 161 | 162 | } -------------------------------------------------------------------------------- /devicetypes/haruny/VirtualADTAlarmSystem.src/VirtualADTAlarmSystem.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * ADT Alarm SmartApp 3 | * 4 | * v.0.0.3 5 | * Copyright 2018 HARUN YAYLI 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 8 | * in compliance with the License. You may obtain a copy of the License at: 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 13 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License 14 | * for the specific language governing permissions and limitations under the License. 15 | * 16 | */ 17 | definition( 18 | name: "ADT Alarm SmartApp", 19 | namespace: "haruny", 20 | author: "Harun Yayli", 21 | description: "The app creates a virtual ADT alarm panel and allows you to run routines depending on the alarm status.\r\nTo be used in junction with ADT MQTT Bridge.\r\nhttps://github.com/haruny/adt-pulse-mqtt", 22 | category: "Safety & Security", 23 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", 24 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", 25 | iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") 26 | 27 | 28 | preferences { 29 | page(name: "selectActions") 30 | } 31 | 32 | def selectActions() { 33 | dynamicPage(name: "selectActions", title: "Select what happens when the alarm turns into the specified state", install: true, uninstall: true) { 34 | def actions = location.helloHome?.getPhrases()*.label 35 | 36 | section("Select Hub"){ 37 | input "theHub", "hub", title: "Select the hub for the alarm device to be installed ", multiple: false, required: true 38 | } 39 | if (actions) { 40 | actions.sort() 41 | 42 | section("Disarm Actions") { 43 | input "actionDisarm", "enum", title: "Select a routine to execute when disarm:", options: actions, required: false 44 | } 45 | section("Disarm Trigger Routine") { 46 | input "triggerDisarm", "enum", title: "Select a routine to disarm the alarm after its triggered:", options: actions, required: false 47 | } 48 | section("Stay Actions") { 49 | input "actionStay", "enum", title: "Select a routine to execute when armed as Stay:", options: actions, required: false 50 | } 51 | section("Stay Trigger Routine") { 52 | input "triggerStay", "enum", title: "Select a routine to arm the alarm as stay after its triggered:", options: actions, required: false 53 | } 54 | section("Away Actions") { 55 | input "actionAway", "enum", title: "Select a routine to execute when armed as Away:", options: actions, required: false 56 | } 57 | 58 | section("Away Trigger Routine") { 59 | input "triggerAway", "enum", title: "Select a routine to arm the alarm as Away after its triggered:", options: actions, required: false 60 | } 61 | section("Alarm Actions") { 62 | input "actionAlarm", "enum", title: "Select a routine to execute when alarm is triggered", options: actions, required: false 63 | } 64 | } 65 | else{ 66 | section("No Routines found!"){ 67 | paragraph "No Routines found. Create routines and try again." 68 | } 69 | } 70 | 71 | section("Finally"){ 72 | paragraph "Don't forget to select the alarm device in the MQTT Bridge app. When you remove this app, Virtual ADT device will be removed. Before removing the app, remove the device from MQTT Bridge" 73 | } 74 | 75 | } 76 | } 77 | 78 | def installed() { 79 | log.debug "Installed with settings: ${settings}" 80 | if (getAllChildDevices().size() == 0) { 81 | def adtDevice = addChildDevice("haruny", "Virtual ADT Alarm System", "adtvas", theHub.id, [completedSetup: true, label: "ADT Alarm System"]) 82 | } 83 | initialize() 84 | } 85 | 86 | def routineChanged(evt) { 87 | 88 | if (settings.triggerStay!=null && settings.triggerStay==evt.displayName){ 89 | log.debug "ADT Alarm App caught an evt: ${evt.displayName} will push Stay button" 90 | getChildDevice("adtvas").stay_push(); 91 | } 92 | 93 | if (settings.triggerDisarm!=null && settings.triggerDisarm==evt.displayName){ 94 | log.debug "ADT Alarm App caught an evt: ${evt.displayName} will push Disarm button" 95 | getChildDevice("adtvas").off_push(); 96 | } 97 | 98 | if (settings.triggerAway!=null && settings.triggerAway==evt.displayName){ 99 | log.debug "ADT Alarm App caught an evt: ${evt.displayName} will push Away button" 100 | getChildDevice("adtvas").away_push(); 101 | } 102 | 103 | 104 | } 105 | 106 | def uninstalled() { 107 | getAllChildDevices().each { 108 | deleteChildDevice(it.deviceNetworkId) 109 | } 110 | } 111 | 112 | def updated() { 113 | log.debug "Updated with settings: ${settings}" 114 | 115 | unsubscribe() 116 | initialize() 117 | } 118 | 119 | def adtEventHandlerMethod(evt){ 120 | log.debug "Alarm had an event ${evt.value}" 121 | 122 | switch (evt.value){ 123 | case "off": 124 | if (settings.actionDisarm!=null){ 125 | log.debug "Calling debug routine ${settings.actionDisarm}" 126 | location.helloHome?.execute(settings.actionDisarm) 127 | } 128 | break; 129 | case "stay": 130 | if (settings.actionStay!=null){ 131 | log.debug "Calling debug routine ${settings.actionStay}" 132 | location.helloHome?.execute(settings.actionStay) 133 | } 134 | break; 135 | case "away": 136 | if (settings.actionAway!=null){ 137 | log.debug "Calling debug routine ${settings.actionAway}" 138 | location.helloHome?.execute(settings.actionAway) 139 | } 140 | break; 141 | case "triggered": 142 | if (settings.actionAlarm!=null){ 143 | log.debug "Calling debug routine ${settings.actionAlarm}" 144 | location.helloHome?.execute(settings.actionAlarm) 145 | } 146 | break; 147 | default: 148 | log.debug "Unknown event ${evt.value}" 149 | break; 150 | 151 | } 152 | 153 | } 154 | def initialize() { 155 | if (getAllChildDevices().size() != 0) { 156 | getAllChildDevices().each{ 157 | subscribe(it, "alarm", "adtEventHandlerMethod") 158 | } 159 | } 160 | subscribe(location, "routineExecuted", routineChanged) 161 | 162 | } 163 | -------------------------------------------------------------------------------- /adt-pulse-mqtt/adt-pulse.js: -------------------------------------------------------------------------------- 1 | // Forked from https://github.com/kevinmhickey/adt-pulse 2 | 3 | var tough = require('tough-cookie'); 4 | var request = require('request'); 5 | var q = require('q'); 6 | var cheerio = require('cheerio'); 7 | var _ = require('lodash'); 8 | 9 | //Cookie jar 10 | var j; 11 | var ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36'; 12 | var sat = ''; 13 | var lastsynckey = ''; 14 | var deviceUpdateCB = function () {}; 15 | var zoneUpdateCB = function () {}; 16 | var statusUpdateCB = function () {}; 17 | 18 | pulse = function(username, password) { 19 | 20 | this.authenticated = false; 21 | this.isAuthenticating = false; 22 | this.clients = []; 23 | 24 | this.configure({ 25 | username: username, 26 | password: password 27 | }); 28 | 29 | /* heartbeat */ 30 | var pulseInterval = setInterval(this.sync.bind(this),5000); 31 | }; 32 | 33 | module.exports = pulse; 34 | 35 | (function() { 36 | 37 | this.config = { 38 | baseUrl: 'https://portal.adtpulse.com', 39 | prefix: '/myhome/13.0.0-153', // you don't need to change this every time. Addon automatically grabs the latest one on the first call. 40 | initialURI: '/', 41 | signinURI: '/access/signin.jsp', 42 | authURI: '/access/signin.jsp?e=n&e=n&&partner=adt', 43 | sensorURI: '/ajax/homeViewDevAjax.jsp', 44 | sensorOrbURI: '/ajax/orb.jsp', 45 | summaryURI: '/summary/summary.jsp', 46 | statusChangeURI: '/quickcontrol/serv/ChangeVariableServ', 47 | armURI: '/quickcontrol/serv/RunRRACommand', 48 | disarmURI: '/quickcontrol/armDisarm.jsp?href=rest/adt/ui/client/security/setArmState', 49 | otherStatusURI: '/ajax/currentStates.jsp', 50 | syncURI: '/Ajax/SyncCheckServ', 51 | logoutURI: '/access/signout.jsp', 52 | 53 | orbUrl: 'https://portal.adtpulse.com/myhome/9.7.0-31/ajax/orb.jsp' // not used 54 | }; 55 | 56 | this.configure = function(options) { 57 | for(o in options){ 58 | this.config[o] = options[o]; 59 | } 60 | }; 61 | 62 | this.login = function () { 63 | 64 | var deferred = q.defer(); 65 | var that = this; 66 | 67 | if(this.authenticated){ 68 | deferred.resolve() 69 | } else { 70 | console.log((new Date()).toLocaleString()+' Pulse: Login called Authenticating'); 71 | 72 | j = request.jar(); 73 | 74 | that.isAuthenticating = true; 75 | request( 76 | { 77 | url: this.config.baseUrl+this.config.initialURI, // call with no prefix to grab the prefix 78 | jar: j, 79 | headers: { 80 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 81 | 'User-Agent': ua 82 | }, 83 | }, 84 | function(e, hResp, b) { 85 | // expecting /myhome/VERSION/access/signin.jsp 86 | if (hResp==null){ 87 | console.log((new Date()).toLocaleString() + ' Pulse: Authentication bad response error:'+JSON.stringify(e)); 88 | that.authenticated =false; 89 | that.isAuthenticating = false; 90 | deferred.reject(); 91 | return deferred.promise; 92 | } 93 | console.log((new Date()).toLocaleString() + ' Pulse: Authentication Received Pathname: '+hResp.request.uri.pathname); 94 | 95 | var uriPart = hResp.request.uri.pathname.match(/\/myhome\/(.+?)\/access/)[1]; 96 | console.log((new Date()).toLocaleString() + ' Pulse: Authentication Page Version: '+uriPart); 97 | that.config.prefix= '/myhome/'+uriPart; 98 | console.log((new Date()).toLocaleString() + ' Pulse: Authentication New URL Prefix '+ that.config.prefix); 99 | console.log((new Date()).toLocaleString() + ' Pulse: Authentication Calling '+ that.config.baseUrl+that.config.prefix+that.config.authURI); 100 | request.post(that.config.baseUrl+that.config.prefix+that.config.authURI, 101 | { 102 | followAllRedirects: true, 103 | jar: j, 104 | headers: { 105 | 'Host': 'portal.adtpulse.com', 106 | 'User-Agent': ua 107 | }, 108 | form:{ 109 | username: that.config.username, 110 | password: that.config.password 111 | } 112 | }, 113 | function(err, httpResponse, body){ 114 | that.isAuthenticating = false; 115 | if(err || httpResponse.request.path !== that.config.prefix+that.config.summaryURI){ 116 | that.authenticated = false; 117 | console.log((new Date()).toLocaleString() + ' Pulse: Authentication Failed'); 118 | console.log((new Date()).toLocaleString() + ' Pulse: httpResponse:' + httpResponse); 119 | deferred.reject(); 120 | } else { 121 | that.authenticated = true; 122 | console.log((new Date()).toLocaleString() + ' Pulse: Authentication Success'); 123 | deferred.resolve(); 124 | that.updateAll.call(that); 125 | } 126 | } 127 | ); 128 | 129 | 130 | } 131 | ); 132 | } 133 | 134 | 135 | 136 | return deferred.promise 137 | }, 138 | 139 | this.logout = function () { 140 | 141 | var that = this; 142 | 143 | console.log((new Date()).toLocaleString() + ' Pulse: Logout'); 144 | 145 | request( 146 | { 147 | url: this.config.baseUrl+this.config.prefix+this.config.logoutURI, 148 | jar: j, 149 | headers: { 150 | 'User-Agent': ua 151 | } 152 | }, 153 | function () { 154 | that.authenticated = false; 155 | } 156 | ) 157 | }, 158 | 159 | this.updateAll = function () { 160 | var that = this; 161 | console.log((new Date()).toLocaleString() + ' Pulse: updateAll'); 162 | 163 | this.getAlarmStatus().then(function(){ 164 | that.getDeviceStatus(); 165 | that.getZoneStatusOrb(); 166 | }); 167 | } 168 | 169 | this.getZoneStatus = function() { 170 | console.log((new Date()).toLocaleString() + ' Pulse.getZoneStatus: Getting Zone Statuses'); 171 | var deferred = q.defer(); 172 | request( 173 | { 174 | url: this.config.baseUrl+this.config.prefix+this.config.sensorURI, 175 | jar: j, 176 | headers: { 177 | 'User-Agent': ua 178 | }, 179 | }, 180 | function(err, httpResponse, body) { 181 | if(err){ 182 | console.log((new Date().toLocaleString()) + ' Pulse.getZoneStatus: Zone JSON Failed'); 183 | } else { 184 | try { 185 | var json = JSON.parse(body.trim()); 186 | if (json != null){ 187 | console.log((new Date().toLocaleString()) + ' DEBUG: Raw JSON:' + json.stringify()); 188 | json.items.forEach(function(obj){ 189 | o = obj; 190 | delete o.deprecatedAction; 191 | o.status = obj.state.icon; 192 | o.statusTxt = obj.state.statusTxt; 193 | o.activityTs = obj.state.activityTs; 194 | delete o.state; 195 | zoneUpdateCB(o); 196 | }) 197 | } else { 198 | console.log((new Date().toLocaleString())+ ' Pulse: No Zone JSON'); 199 | } 200 | } catch(e) { 201 | console.log((new Date().toLocaleString()) + ' Pulse: Invalid Zone JSON'+ e.stack); 202 | } 203 | } 204 | 205 | } 206 | ); 207 | 208 | return deferred.promise; 209 | }, 210 | 211 | this.getZoneStatusOrb = function() { 212 | console.log((new Date()).toLocaleString() + ' Pulse.getZoneStatus(via Orb): Getting Zone Statuses'); 213 | var deferred = q.defer(); 214 | request( 215 | { 216 | url: this.config.baseUrl+this.config.prefix+this.config.sensorOrbURI, 217 | jar: j, 218 | headers: { 219 | 'User-Agent': ua, 220 | 'Referer': this.config.baseUrl+this.config.prefix+this.summaryURI 221 | }, 222 | }, 223 | function(err, httpResponse, body) { 224 | if(err){ 225 | console.log((new Date().toLocaleString()) + ' Pulse.getZoneStatus (via Orb): Zone JSON Failed'); 226 | } else { 227 | // Load response from call to Orb and parse html 228 | const $ = cheerio.load(body); 229 | const sensors = $('#orbSensorsList table tr.p_listRow').toArray(); 230 | // Map values of table to variables 231 | const output = _.map(sensors,(sensor) => { 232 | const theSensor = cheerio.load(sensor); 233 | const theName = theSensor('a.p_deviceNameText').html(); 234 | const theZone = theSensor('span.p_grayNormalText').html(); 235 | const theState = theSensor('span.devStatIcon canvas').attr('icon'); 236 | 237 | const theZoneNumber = (theZone) ? theZone.replace(/(Zone )([0-9]{1,2})/, '$2') : 0; 238 | 239 | let theTag; 240 | 241 | if (theName && theState !== 'devStatUnknown') { 242 | if (theName.includes('Door') || theName.includes('Window')) { 243 | theTag = 'sensor,doorWindow'; 244 | } else if (theName.includes('Glass')) { 245 | theTag = 'sensor,glass'; 246 | } else if (theName.includes('Motion')) { 247 | theTag = 'sensor,motion'; 248 | } else if (theName.includes('Gas')) { 249 | theTag = 'sensor,co'; 250 | } else if (theName.includes('Smoke') || theName.includes('Heat')) { 251 | theTag = 'sensor,fire'; 252 | } 253 | } 254 | /** 255 | * Expected output. 256 | * 257 | * id: sensor-[integer] 258 | * name: device name 259 | * tags: sensor,[doorWindow,motion,glass,co,fire] 260 | * timestamp: timestamp of last activity 261 | * state: devStatOK (device okay) 262 | * devStatOpen (door/window opened) 263 | * devStatMotion (detected motion) 264 | * devStatTamper (glass broken or device tamper) 265 | * devStatAlarm (detected CO/Smoke) 266 | * devStatUnknown (device offline) 267 | */ 268 | timestamp = Math.floor(Date.now() / 1000) // timetamp in seconds 269 | 270 | return { 271 | id: `sensor-${theZoneNumber}`, 272 | name: theName || 'Unknown Sensor', 273 | tags: theTag || 'sensor', 274 | timestamp: timestamp, 275 | state: theState || 'devStatUnknown', 276 | }; 277 | 278 | }); 279 | 280 | console.log((new Date().toLocaleString()) + 'ADT Pulse: Get zone status (via orb) success.'); 281 | output.forEach(function(obj){ 282 | s = obj; 283 | console.log((new Date().toLocaleString()) + ' Sensor: ' + s.id + ' Name: ' + s.name + ' Tags: ' + s.tags + ' State ' + s.state); 284 | zoneUpdateCB(s); 285 | }) 286 | } 287 | } 288 | ); 289 | 290 | return deferred.promise; 291 | }, 292 | 293 | this.getDeviceStatus = function() { // not tested 294 | console.log((new Date()).toLocaleString() + ' Pulse.getDeviceStatus: Getting Device Statuses'); 295 | 296 | request( 297 | { 298 | url: this.config.baseUrl+this.config.prefix+this.config.otherStatusURI, 299 | jar: j, 300 | headers: { 301 | 'User-Agent': ua 302 | }, 303 | }, 304 | function(err, httpResponse, body) { 305 | try{ 306 | $ = cheerio.load(body); 307 | $('tr tr.p_listRow').each(function(el){ 308 | try { 309 | deviceUpdateCB({ 310 | name: $(this).find('td').eq(2).text(), 311 | serialnumber: $(this).find('td').eq(2).find('a').attr('href').split('\'')[1], 312 | state: $(this).find('td').eq(3).text().trim().toLowerCase() == 'off' ? 0 : 1 313 | }) 314 | } 315 | catch (e) { 316 | console.log((new Date()).toLocaleString() + ' Pulse.getDeviceStatus No other devices found'); 317 | } 318 | }) 319 | } 320 | catch(e){ 321 | console.log((new Date()).toLocaleString() + ' Pulse.getDeviceStatus failed: ::'+body+"::"); 322 | } 323 | } 324 | ); 325 | }, 326 | 327 | this.onDeviceUpdate = function (updateCallback) { 328 | deviceUpdateCB = updateCallback; 329 | }, 330 | 331 | this.onZoneUpdate = function (updateCallback) { 332 | zoneUpdateCB = updateCallback; 333 | }, 334 | 335 | this.onStatusUpdate = function (updateCallback) { 336 | statusUpdateCB = updateCallback; 337 | }, 338 | 339 | // not tested 340 | this.deviceStateChange = function (device) { 341 | console.log((new Date()).toLocaleString() + ' Pulse.deviceStateChange: Device State Change', device.name, device.state); 342 | 343 | var deferred = q.defer(); 344 | 345 | request.post(this.config.baseUrl+this.config.prefix+this.config.statusChangeURI + '?fi='+device.serialnumber+'&vn=level&u=On|Off&ft=light-onoff', 346 | 347 | { 348 | followAllRedirects: true, 349 | jar: j, 350 | headers: { 351 | 'Host': 'portal.adtpulse.com', 352 | 'User-Agent': ua, 353 | 'Referer': this.config.baseUrl+this.config.prefix+this.config.summaryURI 354 | }, 355 | form:{ 356 | sat: sat, 357 | value: device.state == 0 ? 'Off' : 'On' 358 | } 359 | }, 360 | function(err, request, body){ 361 | if(err){ 362 | console.log((new Date()).toLocaleString() + ' Pulse: Device State Failure'); 363 | deferred.reject() 364 | } else { 365 | console.log((new Date()).toLocaleString() + ' Pulse: Device State Success'); 366 | deferred.resolve(); 367 | } 368 | } 369 | ); 370 | 371 | return deferred.promise; 372 | }, 373 | 374 | this.getAlarmStatus = function () { 375 | console.log((new Date()).toLocaleString() + ' Pulse.getAlarmStatus: Getting Alarm Statuses'); 376 | var deferred = q.defer(); 377 | 378 | request( 379 | { 380 | url: this.config.baseUrl+this.config.prefix+this.config.summaryURI, 381 | jar: j, 382 | headers: { 383 | 'User-Agent': ua 384 | }, 385 | }, 386 | function(err, httpResponse, body) { 387 | 388 | // signed in? 389 | if (body==null || body.includes("You have not yet signed in")){ 390 | console.log((new Date()).toLocaleString() + ' Pulse: error getting sat login timedout'); 391 | deferred.reject(); 392 | return false; 393 | } 394 | //parse the html 395 | try{ 396 | $ = cheerio.load(body); 397 | statusUpdateCB({ status: $('#divOrbTextSummary span').text()}); 398 | deferred.resolve(); 399 | } 400 | catch(e){ 401 | console.log((new Date()).toLocaleString() + ' Pulse: error getting sat cheerio ::'+ body + '::'+ e); 402 | deferred.reject(); 403 | return false; 404 | } 405 | } 406 | ); 407 | 408 | return deferred.promise; 409 | 410 | }, 411 | 412 | this.setAlarmState = function (action) { 413 | // action can be: stay, away, disarm 414 | // action.newstate 415 | // action.prev_state 416 | 417 | console.log((new Date()).toLocaleString() + ' Pulse.setAlarmState Setting Alarm Status'); 418 | 419 | var deferred = q.defer(); 420 | var that = this; 421 | var url,ref; 422 | 423 | ref = this.config.baseUrl+this.config.prefix+this.config.summaryURI; 424 | 425 | if (action.newstate!='disarm'){ 426 | // we are arming. 427 | if(action.isForced==true){ 428 | url= this.config.baseUrl+this.config.prefix+this.config.armURI+'?sat=' + sat + '&href=rest/adt/ui/client/security/setForceArm&armstate=forcearm&arm=' + encodeURIComponent(action.newstate); 429 | ref= this.config.baseUrl+this.config.prefix+this.config.disarmURI+'&armstate='+ action.prev_state +"&arm="+action.newstate; 430 | } 431 | else{ 432 | url= this.config.baseUrl+this.config.prefix+this.config.disarmURI+'&armstate='+ action.prev_state +"&arm="+action.newstate; 433 | } 434 | } 435 | else{ // disarm 436 | url= this.config.baseUrl+this.config.prefix+this.config.disarmURI+'&armstate='+ action.prev_state +"&arm=off"; 437 | } 438 | 439 | console.log((new Date()).toLocaleString() + ' Pulse.setAlarmState calling the urls:' + url); 440 | 441 | request( 442 | { 443 | url: url, 444 | jar: j, 445 | headers: { 446 | 'User-Agent': ua, 447 | 'Referer': ref 448 | }, 449 | }, 450 | function(err, httpResponse, body) { 451 | if(err){ 452 | console.log((new Date()).toLocaleString() + ' Pulse setAlarmState Failed::'+ body + "::"); 453 | deferred.reject(); 454 | } else { 455 | // when arming check if Some sensors are open or reporting motion 456 | // need the new sat value; 457 | if (action.newstate!="disarm" && action.isForced!=true && body.includes("Some sensors are open or reporting motion")){ 458 | console.log((new Date()).toLocaleString() + ' Pulse setAlarmState Some sensors are open. will force the alarm state'); 459 | 460 | sat = body.match(/sat\=(.+?)&href/)[1]; 461 | console.log((new Date()).toLocaleString() + ' Pulse setAlarmState New SAT ::'+ sat + "::"); 462 | action.isForced=true; 463 | that.setAlarmState(action); 464 | deferred.resolve(body); 465 | } 466 | else{ 467 | // we failed? 468 | // Arming Disarming states are captured. No need to call them failed. 469 | if(!action.isForced && !body.includes("Disarming") && !body.includes("Arming")){ 470 | console.log((new Date()).toLocaleString() + ' Pulse setAlarmState Forced alarm state failed::'+ body + "::"); 471 | deferred.reject(); 472 | } 473 | } 474 | console.log((new Date()).toLocaleString() + ' Pulse setAlarmState Success. Forced?:'+ action.isForced); 475 | deferred.resolve(body); 476 | } 477 | 478 | } 479 | ); 480 | 481 | return deferred.promise; 482 | 483 | } 484 | 485 | this.pulse = function(uid) { 486 | console.log((new Date()).toLocaleString() + ' Pulse.pulse Spanning'); 487 | 488 | if(this.clients.indexOf(uid) >= 0){ 489 | console.log((new Date()).toLocaleString() + ' Pulse: Client Lost', uid); 490 | this.clients.splice(this.clients.indexOf(uid),1) 491 | } else { 492 | console.log((new Date()).toLocaleString() + ' Pulse: New Client', uid); 493 | this.clients.push(uid); 494 | this.sync(); 495 | } 496 | 497 | } 498 | 499 | this.sync = function () { 500 | if(this.clients.length && !this.isAuthenticating){ 501 | var that = this; 502 | this.login().then(function(){ 503 | request({ 504 | url: that.config.baseUrl+that.config.prefix+that.config.syncURI, 505 | jar: j, 506 | followAllRedirects: true, 507 | headers: { 508 | 'User-Agent': ua, 509 | 'Referer': that.config.baseUrl+that.config.prefix+that.config.summaryURI 510 | }, 511 | },function(err, response, body){ 512 | console.log((new Date()).toLocaleString() + ' Pulse.Sync: Syncing', body); 513 | if(err || !body || body.indexOf(" -1){ 514 | that.authenticated = false; 515 | console.log((new Date()).toLocaleString() + ' Pulse.Sync: Sync Failed'); 516 | } else if (lastsynckey != body|| "1-0-0" == body) { 517 | lastsynckey = body; 518 | that.updateAll.call(that); 519 | } 520 | }) 521 | }) 522 | 523 | } else { 524 | console.log((new Date()).toLocaleString() + ' Pulse.Sync: Sync stuck?'); 525 | 526 | } 527 | 528 | } 529 | 530 | }).call(pulse.prototype); 531 | --------------------------------------------------------------------------------