Update to 1.0.0 (#19)

Update to 1.0.0 release

*Update to work with latest ring alarm API changes from @dgreif
*Fix bugs around device publish after disconnect/reconnect
*Add timed republish during startup (should help with auto discovery issues during upgrades and on HASS.io startup
*Lots of minor code refactoring to simplify and move code to functions from main
*Change topic level order to prepare for possibility of supporting non-alarm devices (cameras/lighting)
This commit is contained in:
tsightler 2019-06-14 22:26:01 -04:00 committed by GitHub
parent f22ec856ec
commit aa15c82ba6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 449 additions and 353 deletions

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
npm-debug.log
config.json

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.idea

11
Dockerfile Normal file
View File

@ -0,0 +1,11 @@
FROM node:8
WORKDIR /srv
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "start"]

View File

@ -1,7 +1,7 @@
# ring-alarm-mqtt
This is a simple script that leverages the ring alarm API available at [dgreif/ring-alarm](https://github.com/dgreif/ring-alarm) and provides access to the alarm control panel and sensors via MQTT. It provides support for Home Assistant style MQTT discovery which allows for very easy integration with Home Assistant with near zero configuration (assuming MQTT is already configured). It can also be used with any other tool capable of working with MQTT as it provides consistent topic naming based on location/device ID.
### Installation
### Standard Installation (Linux)
Make sure Node.js (tested with 8.x and higher) is installed on your system and then clone this repo:
`git clone https://github.com/tsightler/ring-alarm-mqtt.git`
@ -15,8 +15,6 @@ npm install
This should install all required dependencies. Edit the config.js and enter your Ring account user/password and MQTT broker connection information. You can also change the top level topic used for creating ring device topics and also configre the Home Assistant state topic, but most people should leave these as default.
Now you should just execute the script and devices should show up automatically in Home Assistant within a few seconds.
### Starting the service automatically during boot
I've included a sample service file which you can use to automaticlly start the script during system boot as long as your system uses systemd (most modern Linux distros). The service file assumes you've installed the script in /opt/ring-alarm-mqtt and that you want to run the process as the homeassistant user, but you can easily modify this to any path and user you'd like. Just edit the file as required and drop it in /etc/systemd/system then run the following:
@ -24,6 +22,29 @@ I've included a sample service file which you can use to automaticlly start the
systemctl enable ring-alarm-mqtt
```
### Docker Installation
To build, execute
```
docker build -t ring-alarm-mqtt/ring-alarm-mqtt .
```
To run, execute
```
docker run -e "MQTTHOST={host name}" -e "MQTTPORT={host port}" -e "MQTTRINGTOPIC={host ring topic}" -e "MQTTHASSTOPIC={host hass topic}" -e "MQTTUSER={mqtt user}" -e "MQTTPASSWORD={mqtt pw}" -e "RINGUSER={ring user}" -e "RINGPASS={ring pq}" ring-alarm-mqtt/ring-alarm-mqtt
```
### Config Options
By default, this script will discover and monitor alarms across all locations, even shared locations for which you have permissions, however, it is possible to limit the locations monitored by the script including specific location IDs in the config as follows:
```"location_ids": ["loc-id", "loc-id2"]```.
To get the location id from the ring website simply login to [Ring.com](https://ring.com/users/sign_in) and look at the address bar in the browser. It will look similar to ```https://app.ring.com/location/{location_id}``` with the last path element being the location id.
Now you should just execute the script and devices should show up automatically in Home Assistant within a few seconds.
### Optional Home Assistant Configuration (Highly Recommended)
If you'd like to take full advantage of the Home Assistant specific features (auto MQTT discovery and server state monitorting) you need to make sure Home Assistant MQTT is configured with discovery and birth message options, here's an example:
```
@ -38,6 +59,24 @@ mqtt:
retain: false
```
### Using with MQTT tools other than Home Assistant (ex: Node Red)
**----------IMPORTANT NOTE----------**
Starting with the 1.0.0 release there is a change in the format of the MQTT topic. This will not impact Home Assistant users as the automatic configuration dynamically builds the topic anyway. However, for those using this script with other MQTT tools and accessing the topics manually, the order of the topic levels has changed slightly, swapping the alarm and location_id levels. Thus, prior to 1.0.0 the topics were formatted as:
```
ring/alarm/<location_id>/<ha_platform_type>/<device_zid>/
```
While in 1.0.0 and future versions it will be:
```
ring/<location_id>/alarm/<ha_platform_type>/<device_zid>/
```
While I was hesitant to make this change because it would break some setups, it seemed like the best thing to do to follow the changes in the ring alarm API from an alarm to a location based model. This will make it more practical to add support for the new non-alarm Ring device which are being added to the API such as smart lighting and cameras while still grouping devices by location like follows:
```
ring/<location_id>/alarm
ring/<location_id>/cameras
ring/<location_id>/lighting
```
### Current Features
- Simple configuration via config file, most cases just need Ring user/password and that's it
- Supports the following devices:
@ -57,13 +96,17 @@ mqtt:
- Monitors MQTT connection and automatically resends device state after any disconnect/reconnect event
- Does not require MQTT retain and can work well with brokers that provide no persistent storage
### Planned features
- Support for non-alarm devices (doorbell/camera motion/lights/siren)
- Support for generic 3rd party sensors
### Possible future features
- Additional Devices (base station, keypad - at least for tamper/battery status)
- Support for smart lighting
- Base station settings (volume, chime)
- Arm/Disarm with code
- Arm/Disarm with sensor bypass
- Dynamic add/remove of alarms/devices (i.e. no service restart required)
- Support for non-alarm devices (doorbell/camera motion/lights/siren)
### Debugging
By default the script should produce no console output, however, the script does leverage the terriffic [debug](https://www.npmjs.com/package/debug) package. To get debug output, simply run the script like this:
@ -80,7 +123,7 @@ DEBUG=ring-alarm-mqtt ./ring-alarm-mqtt.js
This option is also useful when using script with external MQTT tools as it dumps all discovered sensors and their topics. Also allows you to monitor sensor states in real-time on the console.
### Thanks
Much thanks must go to dgrief and his excellent [ring-alarm API](https://github.com/dgreif/ring-alarm) as well as his homebridge plugin. Without his work it would have taken far more effort and time, probably more time than I had, to get this working.
Much thanks must go to @dgrief and his excellent [ring-alarm API](https://github.com/dgreif/ring-alarm) as well as his homebridge plugin. Without his work it would have taken far more effort and time, probably more time than I had, to get this working.
I also have to give much credit to [acolytec3](https://community.home-assistant.io/u/acolytec3) on the Home Assistant community forums for his original Ring Alarm MQTT script. Having an already functioning script with support for MQTT discovery saved me quite a bit of time in developing this script.

200
package-lock.json generated
View File

@ -1,28 +1,28 @@
{
"name": "ring-alarm-mqtt",
"version": "0.8.0",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@dgreif/ring-alarm": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@dgreif/ring-alarm/-/ring-alarm-1.5.0.tgz",
"integrity": "sha512-nFRpZ4q2L9zGIQ/WxkyFWoUMjWMOaGOIVxW1gVLDzED0c1MkijginxXolLeAmhmCZf9QTEnA1GSP9mrVhVdXYQ==",
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@dgreif/ring-alarm/-/ring-alarm-2.2.2.tgz",
"integrity": "sha512-xqp7A2v8EYjeAyXPPvBg6wSSAS1mEGfo3TeZZ8uEMSxiD3FDnV1ySe3InEAh+A4jTcS/8n4Q5lWzj9NPZKQRyg==",
"requires": {
"axios": "^0.18.0",
"axios": "^0.19.0",
"colors": "^1.3.3",
"debug": "^4.1.1",
"rxjs": "^6.4.0",
"rxjs": "^6.5.2",
"socket.io": "^2.2.0"
}
},
"accepts": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
"integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"requires": {
"mime-types": "~2.1.18",
"negotiator": "0.6.1"
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
}
},
"after": {
@ -41,12 +41,12 @@
"integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
},
"axios": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz",
"integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=",
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
"integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
"requires": {
"follow-redirects": "^1.3.0",
"is-buffer": "^1.1.5"
"follow-redirects": "1.5.10",
"is-buffer": "^2.0.2"
}
},
"backo2": {
@ -64,6 +64,11 @@
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
"integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg="
},
"base64-js": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz",
"integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw=="
},
"base64id": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
@ -175,11 +180,12 @@
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"d": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz",
"integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
"integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
"requires": {
"es5-ext": "^0.10.9"
"es5-ext": "^0.10.50",
"type": "^1.0.1"
}
},
"debug": {
@ -188,6 +194,13 @@
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"requires": {
"ms": "^2.1.1"
},
"dependencies": {
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"duplexify": {
@ -229,11 +242,6 @@
"requires": {
"ms": "2.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
@ -262,11 +270,6 @@
"requires": {
"ms": "2.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
@ -283,9 +286,9 @@
}
},
"es5-ext": {
"version": "0.10.49",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.49.tgz",
"integrity": "sha512-3NMEhi57E31qdzmYp2jwRArIUsj1HI/RxbQ4bgnSB+AIKIxsAmTiK83bYMifIcpWvEc3P1X30DhUKOqEtF/kvg==",
"version": "0.10.50",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.50.tgz",
"integrity": "sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw==",
"requires": {
"es6-iterator": "~2.0.3",
"es6-symbol": "~3.1.1",
@ -351,19 +354,19 @@
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"follow-redirects": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz",
"integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==",
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"requires": {
"debug": "^3.2.6"
"debug": "=3.1.0"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "^2.1.1"
"ms": "2.0.0"
}
}
}
@ -374,9 +377,9 @@
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"glob": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz",
"integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@ -465,9 +468,9 @@
}
},
"is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz",
"integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw=="
},
"is-extglob": {
"version": "2.1.1",
@ -524,16 +527,16 @@
"integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA="
},
"mime-db": {
"version": "1.38.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz",
"integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg=="
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
},
"mime-types": {
"version": "2.1.22",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz",
"integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==",
"version": "2.1.24",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
"requires": {
"mime-db": "~1.38.0"
"mime-db": "1.40.0"
}
},
"minimatch": {
@ -550,10 +553,11 @@
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
},
"mqtt": {
"version": "2.18.8",
"resolved": "https://registry.npmjs.org/mqtt/-/mqtt-2.18.8.tgz",
"integrity": "sha512-3h6oHlPY/yWwtC2J3geraYRtVVoRM6wdI+uchF4nvSSafXPZnaKqF8xnX+S22SU/FcgEAgockVIlOaAX3fkMpA==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mqtt/-/mqtt-3.0.0.tgz",
"integrity": "sha512-0nKV6MAc1ibKZwaZQUTb3iIdT4NVpj541BsYrqrGBcQdQ7Jd0MnZD1/6/nj1UFdGTboK9ZEUXvkCu2nPCugHFA==",
"requires": {
"base64-js": "^1.3.0",
"commist": "^1.0.0",
"concat-stream": "^1.6.2",
"end-of-stream": "^1.4.1",
@ -561,35 +565,35 @@
"help-me": "^1.0.1",
"inherits": "^2.0.3",
"minimist": "^1.2.0",
"mqtt-packet": "^5.6.0",
"mqtt-packet": "^6.0.0",
"pump": "^3.0.0",
"readable-stream": "^2.3.6",
"reinterval": "^1.1.0",
"split2": "^2.1.1",
"split2": "^3.1.0",
"websocket-stream": "^5.1.2",
"xtend": "^4.0.1"
}
},
"mqtt-packet": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-5.6.0.tgz",
"integrity": "sha512-QECe2ivqcR1LRsPobRsjenEKAC3i1a5gmm+jNKJLrsiq9PaSQ18LlKFuxvhGxWkvGEPadWv6rKd31O4ICqS1Xw==",
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-6.1.2.tgz",
"integrity": "sha512-yVG5PoS3wJ8TLzfS8pQMsDVLAf/EipnBAG5XQE9X/9L0EMxuduI9J2WnlRvJT497K1CUT4VJWjoP08+CKiKt1Q==",
"requires": {
"bl": "^1.2.1",
"bl": "^1.2.2",
"inherits": "^2.0.3",
"process-nextick-args": "^2.0.0",
"safe-buffer": "^5.1.0"
"safe-buffer": "^5.1.2"
}
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"negotiator": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"next-tick": {
"version": "1.0.0",
@ -710,9 +714,9 @@
"integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8="
},
"rxjs": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz",
"integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==",
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz",
"integrity": "sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==",
"requires": {
"tslib": "^1.9.0"
}
@ -768,11 +772,6 @@
"requires": {
"ms": "2.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
@ -793,20 +792,27 @@
"requires": {
"ms": "2.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
"split2": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz",
"integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/split2/-/split2-3.1.1.tgz",
"integrity": "sha512-emNzr1s7ruq4N+1993yht631/JH+jaj0NYBosuKmLcq+JkGQ9MmTw1RB1fGaTCzUuseRIClrlSLHRNYGwWQ58Q==",
"requires": {
"through2": "^2.0.2"
"readable-stream": "^3.0.0"
},
"dependencies": {
"readable-stream": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz",
"integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==",
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
}
}
},
"stream-shift": {
@ -855,9 +861,14 @@
"integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA="
},
"tslib": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
"integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ=="
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
},
"type": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/type/-/type-1.0.1.tgz",
"integrity": "sha512-MAM5dBMJCJNKs9E7JXo4CXRAansRfG0nlJxW7Wf6GZzSOvH31zClSaHdIMWLehe/EGMBkqeC55rrkaOr5Oo7Nw=="
},
"typedarray": {
"version": "0.0.6",
@ -889,14 +900,14 @@
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"websocket-stream": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/websocket-stream/-/websocket-stream-5.1.2.tgz",
"integrity": "sha512-lchLOk435iDWs0jNuL+hiU14i3ERSrMA0IKSiJh7z6X/i4XNsutBZrtqu2CPOZuA4G/zabiqVAos0vW+S7GEVw==",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/websocket-stream/-/websocket-stream-5.5.0.tgz",
"integrity": "sha512-EXy/zXb9kNHI07TIMz1oIUIrPZxQRA8aeJ5XYg5ihV8K4kD1DuA+FY6R96HfdIHzlSzS8HiISAfrm+vVQkZBug==",
"requires": {
"duplexify": "^3.5.1",
"inherits": "^2.0.1",
"readable-stream": "^2.3.3",
"safe-buffer": "^5.1.1",
"safe-buffer": "^5.1.2",
"ws": "^3.2.0",
"xtend": "^4.0.0"
},
@ -943,4 +954,3 @@
}
}
}

View File

@ -1,12 +1,12 @@
{
"name": "ring-alarm-mqtt",
"version": "0.8.0",
"version": "1.0.0",
"description": "Ring Alarm to MQTT Bridge",
"main": "ring-alarm-mqtt.js",
"dependencies": {
"@dgreif/ring-alarm": "^1.5.0",
"@dgreif/ring-alarm": "^2.2.0",
"debug": "^4.1.1",
"mqtt": "^2.18.8"
"mqtt": "^3.0.0"
},
"devDependencies": {},
"scripts": {
@ -23,4 +23,3 @@
"author": "Tom Sightler (tsightler@gmail.com)",
"license": "MIT"
}

361
ring-alarm-mqtt.js Normal file → Executable file
View File

@ -1,7 +1,7 @@
#!/usr/bin/env node
// Defines
var getAlarms = require('@dgreif/ring-alarm').getAlarms
var getLocations = require('@dgreif/ring-alarm').getLocations
const mqttApi = require ('mqtt')
const debug = require('debug')('ring-alarm-mqtt')
const debugError = require('debug')('error')
@ -11,7 +11,13 @@ var CONFIG
var ringTopic
var hassTopic
var mqttClient
var ringAlarms
var mqttConnected = false
var ringLocations = new Array()
var subscribedLocations = new Array()
var subscribedDevices = new Array()
var publishEnabled = true // Flag to stop publish/republish if connection is down
var republishCount = 10 // Republish config/state this many times after startup or HA start/restart
var republishDelay = 30 // Seconds
// Setup Exit Handwlers
process.on('exit', processExit.bind(null, {cleanup:true, exit:true}))
@ -22,95 +28,97 @@ process.on('uncaughtException', processExit.bind(null, {exit:true}))
/* Functions */
// Simple sleep to pause in async functions
function sleep(ms) {
return new Promise(res => setTimeout(res, ms));
function sleep(sec) {
return new Promise(res => setTimeout(res, sec*1000));
}
// Set unreachable status on exit
async function processExit(options, exitCode) {
if (options.cleanup) {
ringAlarms.map(async alarm => {
availabilityTopic = ringTopic+'/alarm/'+alarm.locationId+'/status'
ringLocations.forEach(async location => {
availabilityTopic = ringTopic+'/'+location.locationId+'/status'
mqttClient.publish(availabilityTopic, 'offline')
})
}
if (exitCode || exitCode === 0) debug('Exit code: '+exitCode)
if (options.exit) {
await sleep(1000)
await sleep(1)
process.exit()
}
}
// Monitor Alarm websocket connection and register/refresh status on connect/disconnect
async function monitorAlarmConnection(alarm) {
alarm.onConnected.subscribe(async connected => {
const devices = await alarm.getDevices()
// Check if location has alarm panel (could be only camera/lights)
async function hasAlarm(location) {
const devices = await location.getDevices()
if (devices.filter(device => device.data.deviceType === 'security-panel')) {
return true
}
return false
}
// Establich websocket connections and register/refresh location status on connect/disconnect
async function processLocations(locations) {
ringLocations.forEach(async location => {
if (!(subscribedLocations.includes(location.locationId)) && await hasAlarm(location)) {
subscribedLocations.push(location.locationId)
location.onConnected.subscribe(async connected => {
if (connected) {
debug('Alarm location '+alarm.locationId+' is connected')
await createAlarm(alarm)
debug('Location '+location.locationId+' is connected')
publishEnabled = true
publishAlarm(location)
} else {
const availabilityTopic = ringTopic+'/alarm/'+alarm.locationId+'/status'
publishEnabled = false
const availabilityTopic = ringTopic+'/'+location.locationId+'/status'
mqttClient.publish(availabilityTopic, 'offline', { qos: 1 })
debug('Alarm location '+alarm.locationId+' is disconnected')
debug('Location '+location.locationId+' is disconnected')
}
})
} else {
publishAlarm(location)
}
})
}
// Return class information if supported device
function supportedDevice(deviceType) {
switch(deviceType) {
function supportedDevice(device) {
switch(device.data.deviceType) {
case 'sensor.contact':
return {
className: 'door',
component: 'binary_sensor'
}
device.className = 'door'
device.component = 'binary_sensor'
break;
case 'sensor.motion':
return {
className: 'motion',
component: 'binary_sensor'
}
device.className = 'motion'
device.component = 'binary_sensor'
break;
case 'alarm.smoke':
return {
className: 'smoke',
component: 'binary_sensor'
}
device.className = 'smoke'
device.component = 'binary_sensor'
break;
case 'alarm.co':
return {
className: 'gas',
component: 'binary_sensor'
}
device.className = 'gas'
device.component = 'binary_sensor'
break;
case 'listener.smoke-co':
return {
classNames: [ 'smoke', 'gas' ],
component: 'binary_sensor'
}
device.classNames = [ 'smoke', 'gas' ]
device.suffixNames = [ 'Smoke', 'CO' ]
device.component = 'binary_sensor'
break;
case 'sensor.flood-freeze':
return {
classNames: [ 'moisture', 'cold' ],
component: 'binary_sensor'
}
device.classNames = [ 'moisture', 'cold' ]
device.suffixNames = [ 'Flood', 'Freeze' ]
device.component = 'binary_sensor'
break;
case 'security-panel':
return {
component: 'alarm_control_panel',
command: true
}
device.component = 'alarm_control_panel'
device.command = true
break;
}
// Check if device is a lock
if (/^lock($|\.)/.test(deviceType)) {
return {
component: 'lock',
command: true
if (/^lock($|\.)/.test(device.data.deviceType)) {
device.component = 'lock'
device.command = true
}
}
return null
}
function getBatteryLevel(device) {
@ -127,75 +135,65 @@ function getBatteryLevel(device) {
return 0
}
// Loop through alarm devices and create/publish MQTT device topics/messages
async function createAlarm(alarm) {
// Loop through alarm devices at location and publish each one
async function publishAlarm(location) {
if (republishCount < 1) { republishCount = 1 }
while (republishCount > 0 && publishEnabled && mqttConnected) {
try {
const availabilityTopic = ringTopic+'/alarm/'+alarm.locationId+'/status'
const devices = await alarm.getDevices()
const availabilityTopic = ringTopic+'/'+location.locationId+'/status'
const devices = await location.getDevices()
devices.forEach((device) => {
const supportedDeviceInfo = supportedDevice(device.data.deviceType)
if (supportedDeviceInfo) {
createDevice(device, supportedDeviceInfo)
supportedDevice(device)
if (device.component) {
publishDevice(device)
}
})
await sleep(1000)
await sleep(1)
mqttClient.publish(availabilityTopic, 'online', { qos: 1 })
} catch (error) {
debugError(error)
}
await sleep(republishDelay)
republishCount--
}
}
// Register alarm devices via HomeAssistant MQTT Discovery and
// subscribe to command topic if control panel to allow actions on arm/disarm messages
async function createDevice(device, supportedDeviceInfo) {
const alarmId = device.alarm.locationId
const deviceId = device.data.zid
const component = supportedDeviceInfo.component
const numSensors = (!supportedDeviceInfo.classNames) ? 1 : supportedDeviceInfo.classNames.length
// Register all device sensors via HomeAssistant MQTT Discovery and
// subscribe to command topic if device accepts commands
async function publishDevice(device) {
const locationId = device.location.locationId
const numSensors = (!device.classNames) ? 1 : device.classNames.length
// Build alarm, availability and device topic
const alarmTopic = ringTopic+'/alarm/'+alarmId
const availabilityTopic = alarmTopic+'/status'
const deviceTopic = alarmTopic+'/'+component+'/'+deviceId
const alarmTopic = ringTopic+'/'+locationId+'/alarm'
const availabilityTopic = ringTopic+'/'+locationId+'/status'
const deviceTopic = alarmTopic+'/'+device.component+'/'+device.zid
// Loop through device sensors and publish HA discovery configuration
for(let i=0; i < numSensors; i++) {
// If device has more than one sensor component create suffixes
// to build unique device entries for each sensor
if (numSensors > 1) {
var className = supportedDeviceInfo.classNames[i]
var uniqueId = deviceId+'_'+className
var subTopic = '/'+className
switch(className) {
case 'smoke':
var deviceName = device.data.name+' - Smoke'
break;
case 'gas':
var deviceName = device.data.name+' - CO'
break
case 'moisture':
var deviceName = device.data.name+' - Flood'
break;
case 'cold':
var deviceName = device.data.name+' - Freeze'
break;
}
var className = device.classNames[i]
var deviceName = device.name+' - '+device.suffixNames[i]
var sensorId = device.zid+'_'+className
var sensorTopic = deviceTopic+'/'+className
} else {
var className = supportedDeviceInfo.className
var uniqueId = deviceId
var subTopic = ''
var deviceName = device.data.name
var className = device.className
var deviceName = device.name
var sensorId = device.zid
var sensorTopic = deviceTopic
}
// Build state topic and HASS MQTT discovery topic
const stateTopic = deviceTopic+subTopic+'/state'
const stateTopic = sensorTopic+'/state'
const attributesTopic = deviceTopic+'/attributes'
const configTopic = 'homeassistant/'+component+'/'+alarmId+'/'+uniqueId+'/config'
const configTopic = 'homeassistant/'+device.component+'/'+locationId+'/'+sensorId+'/config'
// Build the MQTT discovery message
const message = {
name: deviceName,
unique_id: uniqueId,
unique_id: sensorId,
availability_topic: availabilityTopic,
payload_available: 'online',
payload_not_available: 'offline',
@ -205,8 +203,8 @@ async function createDevice(device, supportedDeviceInfo) {
// If device supports commands then
// build command topic and subscribe for updates
if (supportedDeviceInfo.command) {
const commandTopic = deviceTopic+subTopic+'/command'
if (device.command) {
const commandTopic = sensorTopic+'/command'
message.command_topic = commandTopic
mqttClient.subscribe(commandTopic)
}
@ -221,13 +219,21 @@ async function createDevice(device, supportedDeviceInfo) {
mqttClient.publish(configTopic, JSON.stringify(message), { qos: 1 })
}
// Give Home Assistant time to configure device before sending first state data
await sleep(2000)
subscribeDevice(device, deviceTopic)
await sleep(2)
// Publish device data and, if newly registered device, subscribe to state updates
if (subscribedDevices.find(subscribedDevice => subscribedDevice.zid === device.zid)) {
publishDeviceData(device.data, deviceTopic)
} else {
device.onData.subscribe(data => {
publishDeviceData(data, deviceTopic)
})
subscribedDevices.push(device)
}
}
// Publish device status and subscribe for state updates from API
function subscribeDevice(device, deviceTopic) {
device.onData.subscribe(data => {
// Publish device state data
function publishDeviceData(data, deviceTopic) {
var deviceState = undefined
switch(data.deviceType) {
case 'sensor.contact':
@ -294,30 +300,30 @@ function subscribeDevice(device, deviceTopic) {
attributes.tamper_status = data.tamperStatus
}
publishMqttState(deviceTopic+'/attributes', JSON.stringify(attributes))
})
}
// Simple function to publish MQTT state messages with debug
function publishMqttState(topic, message) {
debug(topic, message)
mqttClient.publish(topic, message, { qos: 1 })
}
async function trySetAlarmMode(alarm, deviceId, message, delay) {
// Pause before attempting to alarm mode -- used for retries
async function trySetAlarmMode(location, deviceId, message, delay) {
// Pause before attempting to set alarm mode -- used for retries
await sleep(delay)
var alarmTargetMode
debug('Set alarm mode: '+message)
switch(message) {
case 'DISARM':
alarm.disarm();
location.disarm();
alarmTargetMode = 'none'
break
case 'ARM_HOME':
alarm.armHome()
location.armHome()
alarmTargetMode = 'some'
break
case 'ARM_AWAY':
alarm.armAway()
location.armAway()
alarmTargetMode = 'all'
break
default:
@ -325,8 +331,8 @@ async function trySetAlarmMode(alarm, deviceId, message, delay) {
return 'unknown'
}
// Sleep a few seconds and check if alarm entered requested mode
await sleep(2000);
const devices = await alarm.getDevices()
await sleep(2);
const devices = await location.getDevices()
const device = await devices.find(device => device.data.zid === deviceId)
if (device.data.mode == alarmTargetMode) {
debug('Alarm successfully entered mode: '+message)
@ -338,9 +344,9 @@ async function trySetAlarmMode(alarm, deviceId, message, delay) {
}
// Set Alarm Mode on received MQTT command message
async function setAlarmMode(alarm, deviceId, message) {
async function setAlarmMode(location, deviceId, message) {
debug('Received set alarm mode '+message+' for Security Panel Id: '+deviceId)
debug('Alarm Location Id: '+ alarm.locationId)
debug('Location Id: '+ location.locationId)
// Try to set alarm mode and retry after delay if mode set fails
// Initial attempt with no delay
@ -348,7 +354,7 @@ async function setAlarmMode(alarm, deviceId, message) {
var retries = 12
var setAlarmSuccess = false
while (retries-- > 0 && !(setAlarmSuccess)) {
setAlarmSuccess = await trySetAlarmMode(alarm, deviceId, message, delay*1000)
setAlarmSuccess = await trySetAlarmMode(location, deviceId, message, delay)
// On failure delay 10 seconds for next set attempt
delay = 10
}
@ -361,16 +367,16 @@ async function setAlarmMode(alarm, deviceId, message) {
}
// Set lock target state on received MQTT command message
async function setLockTargetState(alarm, deviceId, message) {
async function setLockTargetState(location, deviceId, message) {
debug('Received set lock state '+message+' for lock Id: '+deviceId)
debug('Alarm Location Id: '+ alarm.locationId)
debug('Location Id: '+ location.locationId)
const command = message.toLowerCase()
switch(command) {
case 'lock':
case 'unlock':
alarm.setDeviceInfo(deviceId, {
location.setDeviceInfo(deviceId, {
command: {
v1: [
{
@ -388,25 +394,41 @@ async function setLockTargetState(alarm, deviceId, message) {
// Process received MQTT command
async function processCommand(topic, message) {
var message = message.toString()
if (topic === hassTopic) {
// Republish devices and state after 60 seconds if restart of HA is detected
debug('Home Assistant state topic '+topic+' received message: '+message)
if (message == 'online') {
debug('Resending device config/state in 30 seconds')
// Make sure any existing republish dies
republishCount = 0
await sleep(republishDelay+5)
// Reset republish counter and start publishing config/state
republishCount = 10
processLocations(ringLocations)
debug('Resent device config/state information')
}
} else {
var topic = topic.split('/')
// Parse topic to get alarm/component/device info
const alarmId = topic[topic.length - 4]
const locationId = topic[topic.length - 5]
const component = topic[topic.length - 3]
const deviceId = topic[topic.length - 2]
// Get alarm by location ID
const alarm = await ringAlarms.find(alarm => alarm.locationId == alarmId)
const location = await ringLocations.find(location => location.locationId == locationId)
switch(component) {
case 'alarm_control_panel':
setAlarmMode(alarm, deviceId, message)
setAlarmMode(location, deviceId, message)
break;
case 'lock':
setLockTargetState(alarm, deviceId, message)
setLockTargetState(location, deviceId, message)
break;
default:
debug('Somehow received command for an unknown device!')
}
}
}
function initMqtt() {
@ -421,36 +443,49 @@ function initMqtt() {
/* End Functions */
// Get Configuration from file
try {
// Main code loop
const main = async() => {
let locationIds = null
// Get Configuration from file
try {
CONFIG = require('./config')
ringTopic = CONFIG.ring_topic ? CONFIG.ring_topic : 'ring'
hassTopic = CONFIG.hass_topic
} catch (e) {
console.error('No configuration file found!')
debugError(e)
process.exit(1)
}
// Establish MQTT connection, subscribe to topics, and handle messages
const main = async() => {
var mqttConnected = false
if (!(CONFIG.location_ids === undefined || CONFIG.location_ids == 0)) {
locationIds = CONFIG.location_ids
}
} catch (e) {
try {
// Get alarms via API
ringAlarms = await getAlarms({
debugError('Configuration file not found, try environment variables!')
CONFIG = {
"host": process.env.MQTTHOST,
"port": process.env.MQTTPORT,
"ring_topic": process.env.MQTTRINGTOPIC,
"hass_topic": process.env.MQTTHASSTOPIC,
"mqtt_user": process.env.MQTTUSER,
"mqtt_pass": process.env.MQTTPASSWORD,
"ring_user": process.env.RINGUSER,
"ring_pass": process.env.RINGPASS
}
ringTopic = CONFIG.ring_topic ? CONFIG.ring_topic : 'ring'
hassTopic = CONFIG.hass_topic
if (!(CONFIG.ring_user || CONFIG.ring_pass)) throw "Required environment variables are not set!"
}
catch (ex) {
debugError(ex)
console.error('Configuration file not found and required environment variables are not set!')
process.exit(1)
}
}
// Establish connection to Ring API
try {
ringLocations = await getLocations({
email: CONFIG.ring_user,
password: CONFIG.ring_pass,
locationIds: locationIds
})
// Start monitoring alarm connection state
ringAlarms.map(async alarm => {
monitorAlarmConnection(alarm)
})
// Connect to MQTT broker
mqttClient = await initMqtt()
mqttConnected = true
} catch (error) {
debugError(error)
debugError( colors.red( 'Couldn\'t create the API instance. This could be because ring.com changed their API again' ))
@ -458,18 +493,26 @@ const main = async() => {
process.exit(1)
}
mqttClient.on('connect', async function () {
if (mqttConnected) {
debugMqtt('Connection established with MQTT broker.')
if (hassTopic) mqttClient.subscribe(hassTopic)
} else {
// Republish device state data after 5 seconds MQTT session reestablished
debugMqtt('MQTT connection reestablished, resending config/state information in 5 seconds.')
await sleep(5000)
ringAlarms.map(async alarm => {
createAlarm(alarm)
})
// Initiate connection to MQTT broker
try {
mqttClient = await initMqtt()
mqttConnected = true
if (hassTopic) { mqttClient.subscribe(hassTopic) }
debugMqtt('Connection established with MQTT broker, sending config/state information in 5 seconds.')
} catch (error) {
debugError(error)
debugError( colors.red( 'Couldn\'t connect to MQTT broker. Please check the broker and configuration settings.' ))
process.exit(1)
}
// On MQTT connect/reconnect send config/state information after delay
mqttClient.on('connect', async function () {
if (!mqttConnected) {
mqttConnected = true
debugMqtt('MQTT connection reestablished, resending config/state information in 5 seconds.')
}
await sleep(5)
processLocations(ringLocations)
})
mqttClient.on('reconnect', function () {
@ -488,21 +531,7 @@ const main = async() => {
// Process MQTT messages from subscribed command topics
mqttClient.on('message', async function (topic, message) {
message = message.toString()
if (topic === hassTopic) {
// Republish devices and state after 60 seconds if restart of HA is detected
debug('Home Assistant state topic '+topic+' received message: '+message)
if (message == 'online') {
debug('Resending device config/state in 60 seconds')
await sleep(60000)
ringAlarms.map(async alarm => {
createAlarm(alarm)
debug('Resent device config/state information')
})
}
} else {
processCommand(topic, message)
}
})
}