diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3128f48 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules +npm-debug.log +config.json \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f52efdf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM node:8 + +WORKDIR /srv + +COPY package*.json ./ + +RUN npm install + +COPY . . + +CMD ["npm", "start"] \ No newline at end of file diff --git a/README.md b/README.md index 0921fcf..fd802ec 100644 --- a/README.md +++ b/README.md @@ -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//// +``` +While in 1.0.0 and future versions it will be: + +``` +ring//alarm/// +``` +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//alarm +ring//cameras +ring//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. diff --git a/package-lock.json b/package-lock.json index 3bd1114..dcf568a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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 @@ } } } - diff --git a/package.json b/package.json index c5bd37e..373fc60 100644 --- a/package.json +++ b/package.json @@ -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" } - diff --git a/ring-alarm-mqtt.js b/ring-alarm-mqtt.js old mode 100644 new mode 100755 index e30f748..3e65793 --- a/ring-alarm-mqtt.js +++ b/ring-alarm-mqtt.js @@ -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() - if (connected) { - debug('Alarm location '+alarm.locationId+' is connected') - await createAlarm(alarm) +// 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('Location '+location.locationId+' is connected') + publishEnabled = true + publishAlarm(location) + } else { + publishEnabled = false + const availabilityTopic = ringTopic+'/'+location.locationId+'/status' + mqttClient.publish(availabilityTopic, 'offline', { qos: 1 }) + debug('Location '+location.locationId+' is disconnected') + } + }) } else { - const availabilityTopic = ringTopic+'/alarm/'+alarm.locationId+'/status' - mqttClient.publish(availabilityTopic, 'offline', { qos: 1 }) - debug('Alarm location '+alarm.locationId+' is disconnected') + 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) { - try { - const availabilityTopic = ringTopic+'/alarm/'+alarm.locationId+'/status' - const devices = await alarm.getDevices() - devices.forEach((device) => { - const supportedDeviceInfo = supportedDevice(device.data.deviceType) - if (supportedDeviceInfo) { - createDevice(device, supportedDeviceInfo) - } - }) - await sleep(1000) - mqttClient.publish(availabilityTopic, 'online', { qos: 1 }) - } catch (error) { - debugError(error) +// 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+'/'+location.locationId+'/status' + const devices = await location.getDevices() + devices.forEach((device) => { + supportedDevice(device) + if (device.component) { + publishDevice(device) + } + }) + 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,103 +219,111 @@ 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 => { - var deviceState = undefined - switch(data.deviceType) { - case 'sensor.contact': - case 'sensor.motion': - var deviceState = data.faulted ? 'ON' : 'OFF' - break; - case 'alarm.smoke': - case 'alarm.co': - var deviceState = data.alarmStatus === 'active' ? 'ON' : 'OFF' - break; - case 'listener.smoke-co': - const coAlarmState = data.co && data.co.alarmStatus === 'active' ? 'ON' : 'OFF' - const smokeAlarmState = data.smoke && data.smoke.alarmStatus === 'active' ? 'ON' : 'OFF' - publishMqttState(deviceTopic+'/gas/state', coAlarmState) - publishMqttState(deviceTopic+'/smoke/state', smokeAlarmState) - break; - case 'sensor.flood-freeze': - const floodAlarmState = data.flood && data.flood.faulted ? 'ON' : 'OFF' - const freezeAlarmState = data.freeze && data.freeze.faulted ? 'ON' : 'OFF' - publishMqttState(deviceTopic+'/moisture/state', floodAlarmState) - publishMqttState(deviceTopic+'/cold/state', freezeAlarmState) - break; - case 'security-panel': - switch(data.mode) { - case 'none': - deviceState = 'disarmed' - break; - case 'some': - deviceState = 'armed_home' - break; - case 'all': - deviceState = 'armed_away' - break; - default: - deviceState = 'unknown' - } - break; - } - - if (/^lock($|\.)/.test(data.deviceType)) { - switch(data.locked) { - case 'locked': - deviceState = 'LOCK' +// Publish device state data +function publishDeviceData(data, deviceTopic) { + var deviceState = undefined + switch(data.deviceType) { + case 'sensor.contact': + case 'sensor.motion': + var deviceState = data.faulted ? 'ON' : 'OFF' + break; + case 'alarm.smoke': + case 'alarm.co': + var deviceState = data.alarmStatus === 'active' ? 'ON' : 'OFF' + break; + case 'listener.smoke-co': + const coAlarmState = data.co && data.co.alarmStatus === 'active' ? 'ON' : 'OFF' + const smokeAlarmState = data.smoke && data.smoke.alarmStatus === 'active' ? 'ON' : 'OFF' + publishMqttState(deviceTopic+'/gas/state', coAlarmState) + publishMqttState(deviceTopic+'/smoke/state', smokeAlarmState) + break; + case 'sensor.flood-freeze': + const floodAlarmState = data.flood && data.flood.faulted ? 'ON' : 'OFF' + const freezeAlarmState = data.freeze && data.freeze.faulted ? 'ON' : 'OFF' + publishMqttState(deviceTopic+'/moisture/state', floodAlarmState) + publishMqttState(deviceTopic+'/cold/state', freezeAlarmState) + break; + case 'security-panel': + switch(data.mode) { + case 'none': + deviceState = 'disarmed' break; - case 'unlocked': - deviceState = 'UNLOCK' + case 'some': + deviceState = 'armed_home' + break; + case 'all': + deviceState = 'armed_away' break; default: - deviceState = 'UNKNOWN' + deviceState = 'unknown' } - } + break; + } - if (deviceState !== undefined) { - publishMqttState(deviceTopic+'/state', deviceState) + if (/^lock($|\.)/.test(data.deviceType)) { + switch(data.locked) { + case 'locked': + deviceState = 'LOCK' + break; + case 'unlocked': + deviceState = 'UNLOCK' + break; + default: + deviceState = 'UNKNOWN' } + } - // Publish any available device attributes (battery, power, etc) - const attributes = {} - batteryLevel = getBatteryLevel(data) - if (batteryLevel !== 'none') { - attributes.battery_level = batteryLevel - } - if (data.tamperStatus) { - attributes.tamper_status = data.tamperStatus - } - publishMqttState(deviceTopic+'/attributes', JSON.stringify(attributes)) - }) + if (deviceState !== undefined) { + publishMqttState(deviceTopic+'/state', deviceState) + } + + // Publish any available device attributes (battery, power, etc) + const attributes = {} + batteryLevel = getBatteryLevel(data) + if (batteryLevel !== 'none') { + attributes.battery_level = batteryLevel + } + if (data.tamperStatus) { + 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,24 +394,40 @@ async function setLockTargetState(alarm, deviceId, message) { // Process received MQTT command async function processCommand(topic, message) { - var topic = topic.split('/') - // Parse topic to get alarm/component/device info - const alarmId = topic[topic.length - 4] - const component = topic[topic.length - 3] - const deviceId = topic[topic.length - 2] + 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 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) + // Get alarm by location ID + const location = await ringLocations.find(location => location.locationId == locationId) - switch(component) { - case 'alarm_control_panel': - setAlarmMode(alarm, deviceId, message) - break; - case 'lock': - setLockTargetState(alarm, deviceId, message) - break; - default: - debug('Somehow received command for an unknown device!') + switch(component) { + case 'alarm_control_panel': + setAlarmMode(location, deviceId, message) + break; + case 'lock': + setLockTargetState(location, deviceId, message) + break; + default: + debug('Somehow received command for an unknown device!') + } } } @@ -421,55 +443,76 @@ function initMqtt() { /* End Functions */ -// 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 +// Main code loop const main = async() => { - var mqttConnected = false + let locationIds = null + + // Get Configuration from file try { - // Get alarms via API - ringAlarms = await getAlarms({ + CONFIG = require('./config') + ringTopic = CONFIG.ring_topic ? CONFIG.ring_topic : 'ring' + hassTopic = CONFIG.hass_topic + if (!(CONFIG.location_ids === undefined || CONFIG.location_ids == 0)) { + locationIds = CONFIG.location_ids + } + } catch (e) { + try { + 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) { + } catch (error) { debugError(error) debugError( colors.red( 'Couldn\'t create the API instance. This could be because ring.com changed their API again' )) debugError( colors.red( 'or maybe the password is wrong. Please check settings and try again.' )) process.exit(1) } + // 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) { - debugMqtt('Connection established with MQTT broker.') - if (hassTopic) mqttClient.subscribe(hassTopic) - } else { - // Republish device state data after 5 seconds MQTT session reestablished + if (!mqttConnected) { + mqttConnected = true debugMqtt('MQTT connection reestablished, resending config/state information in 5 seconds.') - await sleep(5000) - ringAlarms.map(async alarm => { - createAlarm(alarm) - }) } + 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) - } + processCommand(topic, message) }) }