From fe52e0901cf74041a2ee50328257e73bef272da9 Mon Sep 17 00:00:00 2001 From: Marcus Westin Date: Wed, 4 Dec 2019 11:17:06 +0100 Subject: [PATCH] initial commit --- Dockerfile | 67 ++ api.js | 102 +++ build.json | 11 + config.json | 41 + main.js | 68 ++ mqtt.js | 170 ++++ package-lock.json | 1333 ++++++++++++++++++++++++++++ package.json | 12 + plejd.js | 437 +++++++++ rootfs/etc/services.d/plejd/finish | 8 + rootfs/etc/services.d/plejd/run | 14 + rootfs/usr/bin/plejd.sh | 25 + 12 files changed, 2288 insertions(+) create mode 100644 Dockerfile create mode 100644 api.js create mode 100644 build.json create mode 100644 config.json create mode 100644 main.js create mode 100644 mqtt.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 plejd.js create mode 100644 rootfs/etc/services.d/plejd/finish create mode 100644 rootfs/etc/services.d/plejd/run create mode 100644 rootfs/usr/bin/plejd.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..12f0d93 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,67 @@ +ARG BUILD_FROM=hassioaddons/base:5.0.2 +FROM $BUILD_FROM + +ENV LANG C.UTF-8 + +# Set shell +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# Copy data for add-on +COPY ./api.js /plejd/ +COPY ./config.json /plejd/ +COPY ./main.js /plejd/ +COPY ./mqtt.js /plejd/ +COPY ./package.json /plejd/ +COPY ./plejd.js /plejd/ + +ARG BUILD_ARCH + +# Install Node +RUN apk add --no-cache jq +RUN \ + apk add --no-cache --virtual .build-dependencies \ + g++=8.3.0-r0 \ + gcc=8.3.0-r0 \ + libc-dev=0.7.1-r0 \ + linux-headers=4.19.36-r0 \ + make=4.2.1-r2 \ + python=2.7.16-r1 \ + bluez=5.50-r3 \ + eudev-dev=3.2.8-r0 \ + \ + && apk add --no-cache \ + git=2.22.0-r0 \ + nodejs=10.16.3-r0 \ + npm=10.16.3-r0 \ + \ + && npm config set unsafe-perm true + +WORKDIR /plejd +RUN npm install \ + --no-audit \ + --no-update-notifier \ + --unsafe-perm + +# Copy root filesystem +COPY rootfs / + +# Build arguments +ARG BUILD_DATE +ARG BUILD_REF +ARG BUILD_VERSION + +# Labels +LABEL \ + io.hass.name="Plejd" \ + io.hass.description="Adds support for the Swedish home automation devices from Plejd." \ + io.hass.arch="${BUILD_ARCH}" \ + io.hass.type="addon" \ + io.hass.version=${BUILD_VERSION} \ + maintainer="Marcus Westin " \ + org.label-schema.description="Adds support for the Swedish home automation devices from Plejd." \ + org.label-schema.build-date=${BUILD_DATE} \ + org.label-schema.name="Plejd" \ + org.label-schema.schema-version="1.0" \ + org.label-schema.usage="https://github.com/icanos/hassio-plejd/tree/master/README.md" \ + org.label-schema.vcs-ref=${BUILD_REF} \ + org.label-schema.vcs-url="https://github.com/icanos/hassio-plejd" \ No newline at end of file diff --git a/api.js b/api.js new file mode 100644 index 0000000..84a9927 --- /dev/null +++ b/api.js @@ -0,0 +1,102 @@ +const axios = require('axios'); +const EventEmitter = require('events'); +const _ = require('lodash'); + +API_APP_ID = 'zHtVqXt8k4yFyk2QGmgp48D9xZr2G94xWYnF4dak'; +API_BASE_URL = 'https://cloud.plejd.com/parse/'; +API_LOGIN_URL = 'login'; +API_SITES_URL = 'functions/getSites'; + +// #region logging +const debug = ''; + +const getLogger = () => { + const consoleLogger = msg => console.log('plejd-api', msg); + if (debug === 'console') { + return consoleLogger; + } + return _.noop; +}; + +const logger = getLogger(); +// #endregion + +class PlejdApi extends EventEmitter { + constructor(siteName, username, password) { + super(); + + this.siteName = siteName; + this.username = username; + this.password = password; + + this.sessionToken = ''; + this.site = null; + } + + login() { + const self = this; + + const instance = axios.create({ + baseURL: API_BASE_URL, + headers: { + 'X-Parse-Application-Id': API_APP_ID, + 'Content-Type': 'application/json' + } + }); + + instance.post( + API_LOGIN_URL, + { + 'username': this.username, + 'password': this.password + }) + .then((response) => { + self.sessionToken = response.data.sessionToken; + self.emit('loggedIn'); + }); + } + + getCryptoKey(callback) { + const self = this; + + const instance = axios.create({ + baseURL: API_BASE_URL, + headers: { + 'X-Parse-Application-Id': API_APP_ID, + 'X-Parse-Session-Token': this.sessionToken, + 'Content-Type': 'application/json' + } + }); + + instance.post(API_SITES_URL) + .then((response) => { + self.site = response.data.result.find(x => x.site.title == self.siteName); + self.cryptoKey = self.site.plejdMesh.cryptoKey; + + callback(self.cryptoKey); + }) + .catch((error) => { + logger('unable to retrieve the crypto key. error: ' + error); + return Promise.reject('unable to retrieve the crypto key. error: ' + error); + }); + } + + getDevices() { + let devices = []; + + for (let i = 0; i < this.site.devices.length; i++) { + let device = this.site.devices[i]; + let deviceId = device.deviceId; + + devices.push({ + id: this.site.deviceAddress[deviceId], + name: device.title, + type: 'light' + }); + } + + return devices; + } +} + +module.exports = { PlejdApi }; \ No newline at end of file diff --git a/build.json b/build.json new file mode 100644 index 0000000..20efbe1 --- /dev/null +++ b/build.json @@ -0,0 +1,11 @@ +{ + "squash": false, + "build_from": { + "aarch64": "hassioaddons/base-aarch64:5.0.2", + "amd64": "hassioaddons/base-amd64:5.0.2", + "armhf": "hassioaddons/base-armhf:5.0.2", + "armv7": "hassioaddons/base-armv7:5.0.2", + "i386": "hassioaddons/base-i386:5.0.2" + }, + "args": {} +} \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..4b32531 --- /dev/null +++ b/config.json @@ -0,0 +1,41 @@ +{ + "name": "Plejd", + "version": "2", + "slug": "plejd", + "description": "Adds support for the Swedish home automation devices from Plejd.", + "arch": [ + "armhf", + "armv7", + "aarch64", + "amd64", + "i386" + ], + "startup": "application", + "boot": "auto", + "auto_uart": true, + "gpio": true, + "privileged": [ + "SYS_RAWIO" + ], + "devices": [ + "/dev/mem:/dev/mem:rwm" + ], + "apparmor": false, + "host_network": true, + "options": { + "site": "Default Site", + "username": "", + "password": "", + "mqttBroker": "mqtt://", + "mqttUsername": "", + "mqttPassword": "" + }, + "schema": { + "site": "str", + "username": "str", + "password": "str", + "mqttBroker": "str", + "mqttUsername": "str", + "mqttPassword": "str" + } +} \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 0000000..b1f2035 --- /dev/null +++ b/main.js @@ -0,0 +1,68 @@ +const plejd = require('./plejd'); +const api = require('./api'); +const mqtt = require('./mqtt'); +const fs = require('fs'); + +async function main() { + const rawData = fs.readFileSync('/data/plejd.json'); + const config = JSON.parse(rawData); + + const plejdApi = new api.PlejdApi(config.site, config.username, config.password); + const client = new mqtt.MqttClient(config.mqttBroker, config.mqttUsername, config.mqttPassword); + + plejdApi.once('loggedIn', () => { + plejdApi.getCryptoKey((cryptoKey) => { + const devices = plejdApi.getDevices(); + + client.once('connected', () => { + console.log('plejd-mqtt: connected to mqtt.'); + client.discover(devices); + }); + + client.init(); + + // init the BLE interface + const controller = new plejd.Controller(cryptoKey, true); + controller.on('scanComplete', async (peripherals) => { + await controller.connect(); + }); + + controller.on('connected', () => { + console.log('plejd: connected via bluetooth.'); + }); + + // subscribe to changes from Plejd + controller.on('stateChanged', (deviceId, state) => { + client.updateState(deviceId, state); + }); + controller.on('dimChanged', (deviceId, state, dim) => { + client.updateState(deviceId, state); + client.updateBrightness(deviceId, dim); + }); + + // subscribe to changes from HA + client.on('stateChanged', (deviceId, state) => { + if (state) { + controller.turnOn(deviceId); + } + else { + controller.turnOff(deviceId); + } + }); + client.on('brightnessChanged', (deviceId, brightness) => { + if (brightness > 0) { + controller.turnOn(deviceId, brightness); + } + else { + controller.turnOff(deviceId); + } + }); + + controller.init(); + }); + }); + + plejdApi.login(); +} + +main(); \ No newline at end of file diff --git a/mqtt.js b/mqtt.js new file mode 100644 index 0000000..7a54209 --- /dev/null +++ b/mqtt.js @@ -0,0 +1,170 @@ +const EventEmitter = require('events'); +const mqtt = require('mqtt'); +const _ = require('lodash'); + +const startTopic = 'hass/status'; + +// #region logging +const debug = ''; + +const getLogger = () => { + const consoleLogger = msg => console.log('plejd-mqtt', msg); + if (debug === 'console') { + return consoleLogger; + } + return _.noop; +}; + +const logger = getLogger(); +// #endregion + +// #region discovery + +const discoveryPrefix = 'homeassistant'; +const nodeId = 'plejd'; + +const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`; +const getPath = ({ id, type }) => + `${discoveryPrefix}/${type}/${nodeId}/${id}`; +const getConfigPath = plug => `${getPath(plug)}/config`; +const getAvailabilityTopic = plug => `${getPath(plug)}/availability`; +const getStateTopic = plug => `${getPath(plug)}/state`; +const getBrightnessCommandTopic = plug => `${getPath(plug)}/setBrightness`; +const getBrightnessTopic = plug => `${getPath(plug)}/brightness`; +const getCommandTopic = plug => `${getPath(plug)}/set`; + +const getDiscoveryPayload = device => ({ + name: device.name, + unique_id: `light.plejd.${device.name.toLowerCase().replace(/ /g, '')}`, + state_topic: getStateTopic(device), + command_topic: getCommandTopic(device), + brightness_command_topic: getBrightnessCommandTopic(device), + brightness_state_topic: getBrightnessTopic(device), + payload_on: 1, + payload_off: 0, + optimistic: false +}); + +// #endregion + +class MqttClient extends EventEmitter { + constructor(mqttBroker, username, password) { + super(); + + this.mqttBroker = mqttBroker; + this.username = username; + this.password = password; + this.deviceMap = {}; + this.devices = []; + } + + init() { + const self = this; + + this.client = mqtt.connect(this.mqttBroker, { + username: this.username, + password: this.password + }); + + this.client.on('connect', () => { + logger('connected to MQTT.'); + + this.client.subscribe(startTopic, (err) => { + if (err) { + logger('error: unable to subscribe to ' + startTopic); + } + + self.emit('connected'); + }); + + this.client.subscribe(getSubscribePath(), (err) => { + if (err) { + logger('error: unable to subscribe to control topics'); + } + }); + }); + + this.client.on('close', () => { + self.reconnect(); + }); + + this.client.on('message', (topic, message) => { + const command = message.toString(); + + if (topic === startTopic) { + logger('home assistant has started. lets do discovery.'); + self.emit('connected'); + } + + if (_.includes(topic, 'setBrightness')) { + const device = self.devices.find(x => getBrightnessCommandTopic(x) === topic); + logger('got brightness update for ' + device.name + ' with brightness: ' + command); + + self.emit('brightnessChanged', device.id, parseInt(command)); + } + else if (_.includes(topic, 'set') && _.includes(['0', '1'], command)) { + const device = self.devices.find(x => getCommandTopic(x) === topic); + logger('got state update for ' + device.name + ' with state: ' + command); + + self.emit('stateChanged', device.id, parseInt(command)); + } + }); + } + + reconnect() { + this.client.reconnect(); + } + + discover(devices) { + this.devices = devices; + + const self = this; + logger('sending discovery of ' + devices.length + ' device(s).'); + + devices.forEach((device) => { + logger(`sending discovery for ${device.name}`); + + const payload = getDiscoveryPayload(device); + self.deviceMap[device.id] = payload.unique_id; + + self.client.publish( + getConfigPath(device), + JSON.stringify(payload) + ); + }); + } + + updateState(deviceId, state) { + const device = this.devices.find(x => x.id === deviceId); + + if (!device) { + logger('error: ' + deviceId + ' is not handled by us.'); + return; + } + + logger('updating state for ' + device.name + ': ' + state); + + this.client.publish( + getStateTopic(device), + state.toString() + ); + } + + updateBrightness(deviceId, brightness) { + const device = this.devices.find(x => x.id === deviceId); + + if (!device) { + logger('error: ' + deviceId + ' is not handled by us.'); + return; + } + + logger('updating brightness for ' + device.name + ': ' + brightness); + + this.client.publish( + getBrightnessTopic(device), + brightness.toString() + ); + } +} + +module.exports = { MqttClient }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e5094f2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1333 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@abandonware/bluetooth-hci-socket": { + "version": "0.5.3-3", + "resolved": "https://registry.npmjs.org/@abandonware/bluetooth-hci-socket/-/bluetooth-hci-socket-0.5.3-3.tgz", + "integrity": "sha512-SkT5yVoaceiWOK1eWW/QCgAaPph/Y+EkOVBvETmhsD1hhKa83+EmVRMjubO68k54uoIzviqcNhxZI/LuIU2PHA==", + "optional": true, + "requires": { + "debug": "^4.1.0", + "nan": "^2.14.0", + "node-pre-gyp": "^0.13.0", + "usb": "^1.6.0" + } + }, + "@abandonware/noble": { + "version": "1.9.2-5", + "resolved": "https://registry.npmjs.org/@abandonware/noble/-/noble-1.9.2-5.tgz", + "integrity": "sha512-Y1eyxDoA9kvKeAgd6mQ9c4qDbqQbqlPR56LkbtlAqptGB4HT/8KQweqqyTsj4CtdhbvCAt1G+J+2nE35WU9fBg==", + "requires": { + "@abandonware/bluetooth-hci-socket": "^0.5.3-3", + "debug": "^4.1.1", + "napi-thread-safe-callback": "0.0.6", + "node-addon-api": "^1.1.0" + } + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, + "axios": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", + "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", + "requires": { + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-3.0.0.tgz", + "integrity": "sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A==", + "optional": true, + "requires": { + "readable-stream": "^3.0.1" + }, + "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==", + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "buffer-xor": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-2.0.2.tgz", + "integrity": "sha512-eHslX0bin3GB+Lx2p7lEYRShRewuNZL3fUl4qlVJGGiwoPGftmt8JQgk2Y9Ji5/01TnVDo33E5b5O3vUB1HdqQ==", + "requires": { + "safe-buffer": "^5.1.1" + } + }, + "callback-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/callback-stream/-/callback-stream-1.1.0.tgz", + "integrity": "sha1-RwGlEmbwbgbqpx/BcjOCLYdfSQg=", + "requires": { + "inherits": "^2.0.1", + "readable-stream": "> 1.0.0 < 3.0.0" + } + }, + "chownr": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", + "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==", + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "optional": true + }, + "commist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz", + "integrity": "sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==", + "requires": { + "leven": "^2.1.0", + "minimist": "^1.1.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==" + }, + "d": { + "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.50", + "type": "^1.0.1" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, + "requires": { + "mimic-response": "^2.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "optional": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "optional": true + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "es5-ext": { + "version": "0.10.53", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-set": "~0.1.5", + "es6-symbol": "~3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-symbol": "3.1.1", + "event-emitter": "~0.3.5" + }, + "dependencies": { + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + } + } + }, + "es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "requires": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "optional": true + }, + "ext": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", + "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", + "requires": { + "type": "^2.0.0" + }, + "dependencies": { + "type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz", + "integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==" + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "optional": true + }, + "follow-redirects": { + "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.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ=" + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "optional": true + }, + "fs-minipass": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", + "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", + "optional": true, + "requires": { + "minipass": "^2.6.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=", + "optional": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", + "requires": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "optional": true + }, + "help-me": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-1.1.0.tgz", + "integrity": "sha1-jy1QjQYAtKRW2i8IZVbn5cBWo8Y=", + "requires": { + "callback-stream": "^1.0.2", + "glob-stream": "^6.1.0", + "through2": "^2.0.1", + "xtend": "^4.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", + "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "optional": true + }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, + "is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "^2.1.0" + } + }, + "is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=" + }, + "is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "requires": { + "is-unc-path": "^1.0.0" + } + }, + "is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "requires": { + "unc-path-regex": "^0.1.2" + } + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" + }, + "jspack": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/jspack/-/jspack-0.0.4.tgz", + "integrity": "sha1-Mt01x/3LPjRWwY+7fvntC8YjgXc=" + }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=" + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "mimic-response": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.0.0.tgz", + "integrity": "sha512-8ilDoEapqA4uQ3TwS0jakGONKXVJqpy+RpM+3b7pLdOjghCrEiGp9SRkFbUHAmZW9vdnrENWHjaweIoTIJExSQ==", + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "optional": true + }, + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", + "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", + "optional": true, + "requires": { + "minipass": "^2.9.0" + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mqtt": { + "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", + "es6-map": "^0.1.5", + "help-me": "^1.0.1", + "inherits": "^2.0.3", + "minimist": "^1.2.0", + "mqtt-packet": "^6.0.0", + "pump": "^3.0.0", + "readable-stream": "^2.3.6", + "reinterval": "^1.1.0", + "split2": "^3.1.0", + "websocket-stream": "^5.1.2", + "xtend": "^4.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "mqtt-packet": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-6.3.0.tgz", + "integrity": "sha512-TjusSKu8uFZCmGZU8K6r4C3Ic+er/4r8rva+Mp5GQgWg/KjSdAfLK/GTucBwkdATE2Z0Da8AssI2WHzs0M9W0w==", + "requires": { + "bl": "^1.2.2", + "inherits": "^2.0.3", + "process-nextick-args": "^2.0.0", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + } + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "optional": true + }, + "napi-build-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.1.tgz", + "integrity": "sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA==", + "optional": true + }, + "napi-thread-safe-callback": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/napi-thread-safe-callback/-/napi-thread-safe-callback-0.0.6.tgz", + "integrity": "sha512-X7uHCOCdY4u0yamDxDrv3jF2NtYc8A1nvPzBQgvpoSX+WB3jAe2cVNsY448V1ucq7Whf9Wdy02HEUoLW5rJKWg==" + }, + "needle": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", + "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", + "optional": true, + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "optional": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" + }, + "node-abi": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.13.0.tgz", + "integrity": "sha512-9HrZGFVTR5SOu3PZAnAY2hLO36aW1wmA+FDsVkr85BTST32TLCA1H/AEcatVRAsWLyXS3bqUDYCAjq5/QGuSTA==", + "optional": true, + "requires": { + "semver": "^5.4.1" + } + }, + "node-addon-api": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.1.tgz", + "integrity": "sha512-2+DuKodWvwRTrCfKOeR24KIc5unKjOh8mz17NCzVnHWfjAdDqbfbjqh7gUT+BkXBRQM52+xCHciKWonJ3CbJMQ==" + }, + "node-pre-gyp": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.13.0.tgz", + "integrity": "sha512-Md1D3xnEne8b/HGVQkZZwV27WUi1ZRuZBij24TNaZwUPU3ZAFtvT6xxJGaUVillfmMKnn5oD1HoGsp2Ftik7SQ==", + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "noop-logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", + "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=", + "optional": true + }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", + "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", + "optional": true + }, + "npm-packlist": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.6.tgz", + "integrity": "sha512-u65uQdb+qwtGvEJh/DgQgW1Xg7sqeNbmxYyrvlNznaVTjV3E5P6F/EFjM+BVHXl7JJlsdG8A64M0XI8FI/IOlg==", + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "optional": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "ordered-read-streams": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", + "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", + "requires": { + "readable-stream": "^2.0.1" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "optional": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "prebuild-install": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.3.tgz", + "integrity": "sha512-GV+nsUXuPW2p8Zy7SarF/2W/oiK8bFQgJcncoJ0d7kRpekEA0ftChjfEaF9/Y+QJEc/wFR7RAEa8lYByuUIe2g==", + "optional": true, + "requires": { + "detect-libc": "^1.0.3", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "napi-build-utils": "^1.0.1", + "node-abi": "^2.7.0", + "noop-logger": "^0.1.1", + "npmlog": "^4.0.1", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^3.0.3", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0", + "which-pm-runs": "^1.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "optional": true + } + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha1-M2Hs+jymwYKDOA3Qu5VG85D17Oc=" + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "optional": true + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "optional": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "optional": true + }, + "simple-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", + "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=", + "optional": true + }, + "simple-get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", + "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "optional": true, + "requires": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "split2": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.1.1.tgz", + "integrity": "sha512-emNzr1s7ruq4N+1993yht631/JH+jaj0NYBosuKmLcq+JkGQ9MmTw1RB1fGaTCzUuseRIClrlSLHRNYGwWQ58Q==", + "requires": { + "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": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "optional": true + }, + "tar": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", + "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + } + }, + "tar-fs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.0.tgz", + "integrity": "sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA==", + "optional": true, + "requires": { + "chownr": "^1.1.1", + "mkdirp": "^0.5.1", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + } + }, + "tar-stream": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.0.tgz", + "integrity": "sha512-+DAn4Nb4+gz6WZigRzKEZl1QuJVOLtAwwF+WUxy1fJ6X63CaGaUAxJRD2KEn1OMfcbCjySTYpNC6WmfQoIEOdw==", + "optional": true, + "requires": { + "bl": "^3.0.0", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "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==", + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "through2-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", + "requires": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + }, + "to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", + "requires": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "optional": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" + }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" + }, + "unique-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", + "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", + "requires": { + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" + } + }, + "usb": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/usb/-/usb-1.6.2.tgz", + "integrity": "sha512-KcovLXRQuH63iEtnqXyDQGOi5dXHpLM5lZBIUsqSJQToua8nL2sVCieQTkzQBfLe5mCuvk40MgKciI61lgevWw==", + "optional": true, + "requires": { + "bindings": "^1.4.0", + "nan": "2.13.2", + "prebuild-install": "^5.3.3" + }, + "dependencies": { + "nan": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", + "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==", + "optional": true + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "websocket-stream": { + "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.2", + "ws": "^3.2.0", + "xtend": "^4.0.0" + } + }, + "which-pm-runs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", + "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=", + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "requires": { + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0", + "ultron": "~1.1.0" + } + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "optional": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0f3e854 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "dependencies": { + "@abandonware/noble": "^1.9.2-5", + "@abandonware/bluetooth-hci-socket": "0.5.3-3", + "axios": "^0.19.0", + "buffer-xor": "^2.0.2", + "fs": "0.0.1-security", + "jspack": "0.0.4", + "lodash": "^4.17.15", + "mqtt": "^3.0.0" + } +} \ No newline at end of file diff --git a/plejd.js b/plejd.js new file mode 100644 index 0000000..5cf8385 --- /dev/null +++ b/plejd.js @@ -0,0 +1,437 @@ +const noble = require('@abandonware/noble'); +const crypto = require('crypto'); +const xor = require('buffer-xor'); +const _ = require('lodash'); +const EventEmitter = require('events'); + +let debug = ''; + +const getLogger = () => { + const consoleLogger = msg => console.log('plejd', msg); + if (debug === 'console') { + return consoleLogger; + } + + // > /dev/null + return _.noop; +}; + +const logger = getLogger(); + +// UUIDs +const PLEJD_SERVICE = "31ba000160854726be45040c957391b5" +const DATA_UUID = "31ba000460854726be45040c957391b5" +const LAST_DATA_UUID = "31ba000560854726be45040c957391b5" +const AUTH_UUID = "31ba000960854726be45040c957391b5" +const PING_UUID = "31ba000a60854726be45040c957391b5" + +class Controller extends EventEmitter { + constructor(cryptoKey, keepAlive = false) { + super(); + + this.cryptoKey = Buffer.from(cryptoKey.replace(/-/g, ''), 'hex'); + this.peripheral = null; + this.peripheral_address = null; + + this.isScanning = false; + this.isConnecting = false; + this.isConnected = false; + this.keepAlive = keepAlive; + this.writeQueue = []; + this.peripherals = []; + } + + async init() { + const self = this; + + noble.on('stateChange', async (state) => { + logger('ble state changed: ' + state); + + if (state === 'poweredOn') { + await this.scan(); + } + }); + + noble.on('discover', (peripheral) => { + logger('found ' + peripheral.advertisement.localName + ' with addr ' + peripheral.address); + if (peripheral.advertisement.localName === 'P mesh') { + self.peripherals.push(peripheral); + } + }); + } + + async scan() { + const self = this; + this.isScanning = true; + noble.startScanning([PLEJD_SERVICE]); + + setTimeout(() => { + noble.stopScanning(); + this.isScanning = false; + + self.peripherals.sort((a, b) => a.rssi > b.rssi); + this.emit('scanComplete', self.peripherals); + + }, 5000); + } + + async connect() { + const self = this; + + if (this.isScanning) { + logger('already scanning, waiting.'); + + setTimeout(this.connect(), 1000); + return Promise.resolve(false); + } + + if (!this.peripherals.length) { + await this.scan(); + } + + this.isConnecting = true; + + let idx = 0; + return await this._internalConnect(idx); + } + + async _internalConnect(idx) { + const self = this; + + if (idx >= this.peripherals.length) { + logger('reached end of list.'); + return Promise.resolve(false); + } + + logger('connecting to Plejd device'); + try { + this.peripherals[idx].connect(async (err) => { + if (err) { + logger('error: failed to connect to Plejd device: ' + err); + return await self._internalConnect(idx + 1); + } + + self.peripheral = self.peripherals[idx]; + logger('connected to Plejd device with addr ' + self.peripheral.address); + + self.peripheral_address = self._reverseBuffer(Buffer.from(String(self.peripheral.address).replace(/\-/g, '').replace(/\:/g, ''), 'hex')); + + logger('discovering services and characteristics'); + await self.peripheral.discoverSomeServicesAndCharacteristics([PLEJD_SERVICE], [], async (err, services, characteristics) => { + if (err) { + logger('error: failed to discover services: ' + err); + return; + } + + characteristics.forEach((ch) => { + if (DATA_UUID == ch.uuid) { + logger('found DATA characteristic.'); + self.dataCharacteristic = ch; + } + else if (LAST_DATA_UUID == ch.uuid) { + logger('found LAST_DATA characteristic.'); + self.lastDataCharacteristic = ch; + } + else if (AUTH_UUID == ch.uuid) { + logger('found AUTH characteristic.'); + self.authCharacteristic = ch; + } + else if (PING_UUID == ch.uuid) { + logger('found PING characteristic.'); + self.pingCharacteristic = ch; + } + }); + + if (this.dataCharacteristic + && this.lastDataCharacteristic + && this.authCharacteristic + && this.pingCharacteristic) { + + this.on('authenticated', () => { + logger('Plejd is connected and authenticated.'); + + if (self.keepAlive) { + self.startPing(); + } + + self.subscribe(); + + self.emit('connected'); + }); + + try { + await this.authenticate(); + } + catch (error) { + this.isConnecting = false; + logger('error: failed to authenticate: ' + error); + return Promise.resolve(false); + } + + this.isConnected = true; + this.isConnecting = false; + } + + return Promise.resolve(true); + //}); + }); + }); + } + catch (error) { + this.isConnecting = false; + + logger('error: failed to connect to Plejd device: ' + error); + } + + return Promise.resolve(true); + } + + subscribe() { + const self = this; + + self.lastDataCharacteristic.subscribe((err) => { + if (err) { + logger('error: couldnt subscribe to notification characteristic.'); + } + + // subscribe to last data event + self.lastDataCharacteristic.on('data', (data, isNotification) => { + const decoded = self._encryptDecrypt(self.cryptoKey, self.peripheral_address, data); + + let state = 0; + let dim = 0; + let device = parseInt(decoded[0], 10); + + if (decoded.toString('hex', 3, 5) === '00c8' || decoded.toString('hex', 3, 5) === '0098') { + state = parseInt(decoded.toString('hex', 5, 6), 10); + dim = parseInt(decoded.toString('hex', 6, 8), 16) >> 8; + + logger('d: ' + device + ' got state+dim update: ' + state + ' - ' + dim); + this.emit('dimChanged', device, state, dim); + } + else if (decoded.toString('hex', 3, 5) === '0097') { + state = parseInt(decoded.toString('hex', 5, 6), 10); + logger('d: ' + device + ' got state update: ' + state); + this.emit('stateChanged', device, state); + } + }); + }); + } + + async disconnect() { + logger('disconnecting from Plejd'); + + if (this.isConnected) { + clearInterval(this.pingRef); + + if (this.peripheral) { + try { + await this.peripheral.disconnect(); + } + catch (error) { + logger('error: unable to disconnect from Plejd: ' + error); + return Promise.resolve(false); + } + + this.isConnected = false; + logger('disconnected from Plejd'); + + return Promise.resolve(true); + } + } + else { + clearInterval(this.pingRef); + this.isConnected = false; + logger('disconnected from Plejd'); + + return Promise.resolve(true); + } + } + + turnOn(id, brightness) { + if (!this.isConnected) { + logger('error: not connected'); + return; + } + + logger('turning on ' + id + ' at brightness ' + brightness); + + var payload; + + if (!brightness) { + payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009701', 'hex'); + } else { + brightness = brightness << 8 | brightness; + payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009801' + (brightness).toString(16).padStart(4, '0'), 'hex'); + } + + this.write(payload); + } + + turnOff(id) { + if (!this.isConnected) { + logger('error: not connected'); + return; + } + + logger('turning off ' + id); + + var payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009700', 'hex'); + this.write(payload); + } + + startPing() { + const self = this; + + clearInterval(this.pingRef); + logger('starting ping'); + this.pingRef = setInterval(async () => { + logger('ping'); + if (self.isConnected) { + self.plejdPing(async (pingOk) => { + + if (!pingOk) { + logger('ping failed'); + await self.disconnect(); + await self.connect(); + } + else { + logger('pong'); + } + }); + } + else { + await self.disconnect(); + await self.connect(); + } + }, 3000); + } + + plejdPing(callback) { + var ping = crypto.randomBytes(1); + + this.pingCharacteristic.write(ping, false, (err) => { + if (err) { + logger('error: unable to send ping: ' + err); + callback(false); + } + + this.pingCharacteristic.read((err, data) => { + if (err) { + logger('error: unable to read ping: ' + err); + callback(false); + } + + if (((ping[0] + 1) & 0xff) !== data[0]) { + callback(false); + } + else { + callback(true); + } + }); + }); + } + + async authenticate() { + const self = this; + + logger('authenticating connection'); + this.authCharacteristic.write(Buffer.from([0]), false, (err) => { + if (err) { + logger('error: failed to authenticate: ' + err); + return; + } + + this.authCharacteristic.read(async (err2, data) => { + if (err2) { + logger('error: challenge request failed: ' + err2); + return; + } + + var resp = self._challengeResponse(self.cryptoKey, data); + + this.authCharacteristic.write(resp, false, (err3) => { + if (err3) { + logger('error: challenge failed: ' + err2); + return; + } + + this.emit('authenticated'); + }); + }); + }); + } + + async write(data) { + const self = this; + + try { + if (this.isConnecting) { + this.writeQueue.push(data); + return Promise.resolve(true); + } + + if (!this.keepAlive) { + await this.connect(); + } + + await this.dataCharacteristic.write(this._encryptDecrypt(this.cryptoKey, this.peripheral_address, data), false); + + let writeData; + while ((writeData = this.writeQueue.shift()) !== undefined) { + await this.dataCharacteristic.write(this._encryptDecrypt(this.cryptoKey, this.peripheral_address, writeData), false); + } + + if (!this.keepAlive) { + clearTimeout(this.disconnectIntervalRef); + this.disconnectIntervalRef = setTimeout(async () => { + await self.disconnect(); + }, 5000); + } + } + catch (error) { + logger('error when writing to plejd: ' + error); + } + } + + _challengeResponse(key, challenge) { + const intermediate = crypto.createHash('sha256').update(xor(key, challenge)).digest(); + const part1 = intermediate.subarray(0, 16); + const part2 = intermediate.subarray(16); + + const resp = xor(part1, part2); + + return resp; + } + + _encryptDecrypt(key, addr, data) { + var buf = Buffer.concat([addr, addr, addr.subarray(0, 4)]); + + var cipher = crypto.createCipheriv("aes-128-ecb", key, ''); + cipher.setAutoPadding(false); + + var ct = cipher.update(buf).toString('hex'); + ct += cipher.final().toString('hex'); + ct = Buffer.from(ct, 'hex'); + + var output = ""; + for (var i = 0, length = data.length; i < length; i++) { + output += String.fromCharCode(data[i] ^ ct[i % 16]); + } + + return Buffer.from(output, 'ascii'); + } + + _reverseBuffer(src) { + var buffer = Buffer.allocUnsafe(src.length) + + for (var i = 0, j = src.length - 1; i <= j; ++i, --j) { + buffer[i] = src[j] + buffer[j] = src[i] + } + + return buffer + } +} + +module.exports = { Controller }; \ No newline at end of file diff --git a/rootfs/etc/services.d/plejd/finish b/rootfs/etc/services.d/plejd/finish new file mode 100644 index 0000000..cec873a --- /dev/null +++ b/rootfs/etc/services.d/plejd/finish @@ -0,0 +1,8 @@ +#!/usr/bin/execlineb -S0 +# ============================================================================== +# Community Hass.io Add-ons: Plejd +# ============================================================================== +if -n { s6-test $# -ne 0 } +if -n { s6-test ${1} -eq 256 } + +s6-svscanctl -t /var/run/s6/services \ No newline at end of file diff --git a/rootfs/etc/services.d/plejd/run b/rootfs/etc/services.d/plejd/run new file mode 100644 index 0000000..d67b10d --- /dev/null +++ b/rootfs/etc/services.d/plejd/run @@ -0,0 +1,14 @@ +#!/usr/bin/with-contenv bashio +# ============================================================================== +# Community Hass.io Add-ons: Plejd +# Runs the Plejd service +# ============================================================================== + +bashio::log.info 'Starting the Plejd service...' + +# Change working directory +cd /plejd || bashio::exit.nok 'Unable to change working directory' + +# Run the Plejd service +chmod +x /usr/bin/plejd.sh +exec /usr/bin/plejd.sh \ No newline at end of file diff --git a/rootfs/usr/bin/plejd.sh b/rootfs/usr/bin/plejd.sh new file mode 100644 index 0000000..1e9eb43 --- /dev/null +++ b/rootfs/usr/bin/plejd.sh @@ -0,0 +1,25 @@ +#!/usr/bin/with-contenv bashio + +CONFIG_PATH=/data/options.json + +SITE=$(jq --raw-output ".site" $CONFIG_PATH) +USERNAME=$(jq --raw-output ".username" $CONFIG_PATH) +PASSWORD=$(jq --raw-output ".password" $CONFIG_PATH) +MQTTBROKER=$(jq --raw-output ".mqttBroker" $CONFIG_PATH) +MQTTUSERNAME=$(jq --raw-output ".mqttUsername" $CONFIG_PATH) +MQTTPASSWORD=$(jq --raw-output ".mqttPassword" $CONFIG_PATH) + +PLEJD_PATH=/data/plejd.json +PLEJD_CONFIG="{ + \"site\": \"$SITE\", + \"username\": \"$USERNAME\", + \"password\": \"$PASSWORD\", + \"mqttBroker\": \"$MQTTBROKER\", + \"mqttUsername\": \"$MQTTUSERNAME\", + \"mqttPassword\": \"$MQTTPASSWORD\" +} +" + +echo "$PLEJD_CONFIG" > $PLEJD_PATH + +exec node /plejd/main.js \ No newline at end of file