diff --git a/plejd/Dockerfile b/plejd/Dockerfile index 66e1006..bc4cff1 100644 --- a/plejd/Dockerfile +++ b/plejd/Dockerfile @@ -12,7 +12,7 @@ COPY ./config.json /plejd/ COPY ./main.js /plejd/ COPY ./mqtt.js /plejd/ COPY ./package.json /plejd/ -COPY ./plejd.js /plejd/ +COPY ./ble.js /plejd/ ARG BUILD_ARCH diff --git a/plejd/ble.js b/plejd/ble.js new file mode 100644 index 0000000..1663822 --- /dev/null +++ b/plejd/ble.js @@ -0,0 +1,530 @@ +const noble = require('@icanos/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" + +const STATE_IDLE = 'idle'; +const STATE_SCANNING = 'scanning'; +const STATE_CONNECTING = 'connecting'; +const STATE_CONNECTED = 'connected'; +const STATE_AUTHENTICATED = 'authenticated'; +const STATE_DISCONNECTED = 'disconnected'; +const STATE_UNINITIALIZED = 'uninitialized'; +const STATE_INITIALIZED = 'initialized'; + +class PlejdService extends EventEmitter { + constructor(cryptoKey, keepAlive = false) { + super(); + + this.cryptoKey = Buffer.from(cryptoKey.replace(/-/g, ''), 'hex'); + + // Keeps track of the current state + this.state = STATE_IDLE; + // Keeps track of discovered devices + this.devices = {}; + // Keeps track of the currently connected device + this.device = null; + this.deviceAddress = null; + this.deviceIdx = 0; + + this.writeQueue = []; + + // Holds a reference to all characteristics + this.characteristicState = STATE_UNINITIALIZED; + this.characteristics = { + data: null, + lastData: null, + auth: null, + ping: null + }; + + logger('wiring events and waiting for BLE interface to power up.'); + this.wireEvents(); + } + + turnOn(id, brightness) { + 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) { + logger('turning off ' + id); + + var payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009700', 'hex'); + this.write(payload); + } + + scan() { + logger('scan()'); + + if (this.state === STATE_SCANNING) { + console.log('error: already scanning, please wait.'); + return; + } + + this.state = STATE_SCANNING; + noble.startScanning([PLEJD_SERVICE]); + + setTimeout(() => { + noble.stopScanning(); + this.state = STATE_IDLE; + + const foundDeviceCount = Object.values(this.devices).length; + logger('scan completed, found ' + foundDeviceCount + ' device(s).'); + + if (foundDeviceCount == 0) { + console.log('warning: no devices found. will not do anything else.'); + } + else { + this.emit('scanComplete', this.devices); + } + }, 5000); + } + + connect(uuid = null) { + const self = this; + if (this.state === STATE_CONNECTING) { + console.log('warning: currently connecting to a device, please wait...'); + return; + } + + if (!uuid) { + this.device = Object.values(this.devices)[this.deviceIdx]; + } + else { + this.device = this.devices[uuid]; + if (!this.device) { + console.log('error: could not find a device with uuid: ' + uuid); + return; + } + } + + if (!this.device) { + console.log('error: reached end of device list. cannot continue.'); + return; + } + + this.deviceAddress = this._reverseBuffer( + Buffer.from( + String(this.device.address) + .replace(/\-/g, '') + .replace(/\:/g, ''), 'hex' + ) + ); + + logger('connecting to ' + this.device.id + ' with addr ' + this.device.address + ' and rssi ' + this.device.rssi); + setTimeout(() => { + if (self.state !== STATE_CONNECTED && self.state !== STATE_AUTHENTICATED) { + if (self.deviceIdx < Object.keys(self.devices).length) { + logger('connection timed out after 10 s. trying next.'); + + self.deviceIdx++; + self.connect(); + } + } + }, 10 * 1000); + + this.state = STATE_CONNECTING; + this.device.connect((err) => { + self.onDeviceConnected(err); + }); + } + + reset() { + logger('reset()'); + this.state = STATE_IDLE; + } + + disconnect() { + logger('disconnect()'); + if (this.state !== STATE_CONNECTED) { + return; + } + + clearInterval(this.pingRef); + + this.unsubscribeCharacteristics(); + this.device.disconnect(); + + this.state = STATE_DISCONNECTED; + } + + authenticate() { + logger('authenticate()'); + const self = this; + + if (this.state !== STATE_CONNECTED) { + console.log('error: need to be connected and not previously authenticated (new connection).'); + return; + } + + this.characteristics.auth.write(Buffer.from([0]), false, (err) => { + if (err) { + console.log('error: failed to authenticate: ' + err); + return; + } + + self.characteristics.auth.read((err, data) => { + if (err) { + console.log('error: failed to read auth response: ' + err); + return; + } + + var resp = self._createChallengeResponse(self.cryptoKey, data); + self.characteristics.auth.write(resp, false, (err) => { + if (err) { + console.log('error: failed to challenge: ' + err); + return; + } + + self.state = STATE_AUTHENTICATED; + self.emit('authenticated'); + }); + }) + }); + } + + write(data) { + if (this.state !== STATE_AUTHENTICATED) { + logger('error: not connected.'); + this.writeQueue.push(data); + return false; + } + + const encryptedData = this._encryptDecrypt(this.cryptoKey, this.deviceAddress, data); + this.characteristics.data.write(encryptedData, false); + + let writeData; + while ((writeData = this.writeQueue.shift()) !== undefined) { + this.characteristics.data.write(this._encryptDecrypt(this.cryptoKey, this.deviceAddress, writeData), false); + } + } + + onAuthenticated() { + // Start ping + logger('onAuthenticated()'); + this.startPing(); + } + + startPing() { + logger('startPing()'); + clearInterval(this.pingRef); + + this.pingRef = setInterval(async () => { + if (this.state === STATE_AUTHENTICATED) { + logger('ping'); + this.ping(); + } + else if (this.state === STATE_DISCONNECTED) { + console.log('warning: device disconnected, stop ping.'); + } + else { + console.log('error: ping failed, not connected.'); + } + }, 3000); + } + + onPingSuccess(nr) { + logger('pong: ' + nr); + } + + onPingFailed(error) { + logger('onPingFailed(' + error + ')'); + + logger('stopping ping and reconnecting.'); + clearInterval(this.pingRef); + + this.unsubscribeCharacteristics(); + this.state = STATE_DISCONNECTED; + + this.connect(this.device.id); + } + + ping() { + logger('ping()'); + + if (this.state !== STATE_AUTHENTICATED) { + console.log('error: needs to be authenticated before pinging.'); + return; + } + + const self = this; + var ping = crypto.randomBytes(1); + + try { + this.characteristics.ping.write(ping, false, (err) => { + if (err) { + console.log('error: unable to send ping: ' + err); + self.emit('pingFailed'); + return; + } + + this.characteristics.ping.read((err, data) => { + if (err) { + console.log('error: unable to read ping: ' + err); + self.emit('pingFailed'); + return; + } + + if (((ping[0] + 1) & 0xff) !== data[0]) { + self.emit('pingFailed'); + return; + } + else { + self.emit('pingSuccess', data[0]); + } + }); + }); + } + catch (error) { + console.log('error: writing to plejd: ' + error); + self.emit('pingFailed', error); + } + } + + onDeviceConnected(err) { + logger('onDeviceConnected()'); + const self = this; + + if (err) { + console.log('error: failed to connect to device: ' + err + '. picking next.'); + this.deviceIdx++; + this.reset(); + this.connect(); + return; + } + + this.state = STATE_CONNECTED; + + if (this.characteristicState === STATE_UNINITIALIZED) { + // We need to discover the characteristics + logger('discovering services and characteristics'); + + setTimeout(() => { + if (this.characteristicState === STATE_UNINITIALIZED) { + console.log('error: discovering characteristics timed out. trying next device.'); + self.deviceIdx++; + self.disconnect(); + self.connect(); + } + }, 5000); + + this.device.discoverSomeServicesAndCharacteristics([PLEJD_SERVICE], [], async (err, services, characteristics) => { + if (err) { + console.log('error: failed to discover services: ' + err); + return; + } + + if (self.state !== STATE_CONNECTED || self.characteristicState !== STATE_UNINITIALIZED) { + // in case our time out triggered before we got here. + console.log('warning: found characteristics in invalid state. ignoring.'); + return; + } + + logger('found ' + characteristics.length + ' characteristic(s).'); + + characteristics.forEach((ch) => { + if (DATA_UUID == ch.uuid) { + logger('found DATA characteristic.'); + self.characteristics.data = ch; + } + else if (LAST_DATA_UUID == ch.uuid) { + logger('found LAST_DATA characteristic.'); + self.characteristics.lastData = ch; + } + else if (AUTH_UUID == ch.uuid) { + logger('found AUTH characteristic.'); + self.characteristics.auth = ch; + } + else if (PING_UUID == ch.uuid) { + logger('found PING characteristic.'); + self.characteristics.ping = ch; + } + }); + + if (self.characteristics.data + && self.characteristics.lastData + && self.characteristics.auth + && self.characteristics.ping) { + + self.characteristicState = STATE_INITIALIZED; + + // subscribe to notifications + this.subscribeCharacteristics(); + + self.emit('deviceCharacteristicsComplete', self.device); + } + }); + } + } + + onDeviceCharacteristicsComplete(device) { + logger('onDeviceCharacteristicsComplete(' + device.id + ')'); + this.authenticate(); + } + + onDeviceDiscovered(device) { + logger('onDeviceDiscovered(' + device.id + ')'); + if (device.advertisement.localName === 'P mesh') { + logger('device is P mesh'); + this.devices[device.id] = device; + } + } + + onDeviceDisconnected() { + logger('onDeviceDisconnected()'); + + if (!this.device) { + console.log('warning: reconnect will not be performed.'); + return; + } + + // we just want to reconnect + this.connect(this.device.id); + } + + onDeviceScanComplete() { + logger('onDeviceScanComplete()'); + console.log('trying to connect to the mesh network.'); + this.connect(); + } + + onInterfaceStateChanged(state) { + logger('onInterfaceStateChanged(' + state + ')'); + + if (state === 'poweredOn') { + this.scan(); + } + } + + onLastDataUpdated(data, isNotification) { + const decoded = this._encryptDecrypt(this.cryptoKey, this.deviceAddress, 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); + } + } + + wireEvents() { + logger('wireEvents()'); + const self = this; + + noble.on('stateChange', this.onInterfaceStateChanged.bind(self)); + //noble.on('scanStop', this.onDeviceScanComplete.bind(self)); + noble.on('discover', this.onDeviceDiscovered.bind(self)); + noble.on('disconnect', this.onDeviceDisconnected.bind(self)); + + this.on('scanComplete', this.onDeviceScanComplete.bind(this)); + this.on('deviceCharacteristicsComplete', this.onDeviceCharacteristicsComplete.bind(self)); + this.on('authenticated', this.onAuthenticated.bind(self)); + this.on('pingFailed', this.onPingFailed.bind(self)); + this.on('pingSuccess', this.onPingSuccess.bind(self)); + } + + subscribeCharacteristics() { + if (this.characteristics.lastData) { + this.characteristics.lastData.subscribe((err) => { + if (err) { + console.log('error: could not subscribe to event.'); + } + }); + this.characteristics.lastData.on('data', this.onLastDataUpdated.bind(this)); + } + } + + unsubscribeCharacteristics() { + if (this.characteristics.lastData) { + this.characteristics.lastData.unsubscribe((err) => { + if (err) { + console.log('error: could not unsubscribe from event.'); + } + }); + } + } + + _createChallengeResponse(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 = PlejdService; \ No newline at end of file diff --git a/plejd/config.json b/plejd/config.json index 1fb98a4..c7043c2 100644 --- a/plejd/config.json +++ b/plejd/config.json @@ -1,6 +1,6 @@ { "name": "Plejd", - "version": "0.0.7", + "version": "0.1.0", "slug": "plejd", "description": "Adds support for the Swedish home automation devices from Plejd.", "url": "https://github.com/icanos/hassio-plejd/", diff --git a/plejd/main.js b/plejd/main.js index a43c427..78057a5 100644 --- a/plejd/main.js +++ b/plejd/main.js @@ -2,6 +2,7 @@ const plejd = require('./plejd'); const api = require('./api'); const mqtt = require('./mqtt'); const fs = require('fs'); +const PlejdService = require('./ble'); async function main() { const rawData = fs.readFileSync('/data/plejd.json'); @@ -22,43 +23,37 @@ async function main() { client.init(); // init the BLE interface - const controller = new plejd.Controller(cryptoKey, true); - controller.on('scanComplete', async (peripherals) => { - await controller.connect(); - }); - - controller.on('connected', () => { + const plejd = new PlejdService(cryptoKey, true); + plejd.on('authenticated', () => { console.log('plejd: connected via bluetooth.'); }); // subscribe to changes from Plejd - controller.on('stateChanged', (deviceId, state) => { + plejd.on('stateChanged', (deviceId, state) => { client.updateState(deviceId, state); }); - controller.on('dimChanged', (deviceId, state, dim) => { + plejd.on('dimChanged', (deviceId, state, dim) => { client.updateState(deviceId, state); client.updateBrightness(deviceId, dim); }); // subscribe to changes from HA - client.on('stateChanged', async (deviceId, state) => { + client.on('stateChanged', (deviceId, state) => { if (state) { - await controller.turnOn(deviceId); + plejd.turnOn(deviceId); } else { - await controller.turnOff(deviceId); + plejd.turnOff(deviceId); } }); - client.on('brightnessChanged', async (deviceId, brightness) => { + client.on('brightnessChanged', (deviceId, brightness) => { if (brightness > 0) { - await controller.turnOn(deviceId, brightness); + plejd.turnOn(deviceId, brightness); } else { - await controller.turnOff(deviceId); + plejd.turnOff(deviceId); } }); - - controller.init(); }); }); diff --git a/plejd/package-lock.json b/plejd/package-lock.json index 711984e..d1315fa 100644 --- a/plejd/package-lock.json +++ b/plejd/package-lock.json @@ -13,10 +13,10 @@ "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==", + "@icanos/noble": { + "version": "1.9.2-6", + "resolved": "https://registry.npmjs.org/@icanos/noble/-/noble-1.9.2-6.tgz", + "integrity": "sha512-+NxEW7nNEueqX8MknTdno3AZeFode56tzN+dMDW0TJjW96XG822DhoHmHeQZRylTd74r/8M5c4Sb9x/m45yiPw==", "requires": { "@abandonware/bluetooth-hci-socket": "^0.5.3-3", "debug": "^4.1.1", @@ -24,6 +24,11 @@ "node-addon-api": "^1.1.0" } }, + "@types/zen-observable": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.0.tgz", + "integrity": "sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg==" + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -134,6 +139,11 @@ "readable-stream": "> 1.0.0 < 3.0.0" } }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, "chownr": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", @@ -311,6 +321,11 @@ "ext": "^1.1.2" } }, + "esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" + }, "event-emitter": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", @@ -550,6 +565,14 @@ "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=" }, + "is-observable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", + "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==", + "requires": { + "symbol-observable": "^1.1.0" + } + }, "is-relative": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", @@ -818,6 +841,11 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "observable-fns": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/observable-fns/-/observable-fns-0.4.0.tgz", + "integrity": "sha512-2BFtEqza7sjLpgImAmagHK97mBwh3+bkwAZS/qF/4n2S8RzKsbdsdOczRBh+Piz7QgQZRAjTzI5vtxtOUgU+cQ==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1090,6 +1118,11 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, "tar": { "version": "4.4.13", "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", @@ -1142,6 +1175,20 @@ } } }, + "threads": { + "version": "1.0.0-beta.9", + "resolved": "https://registry.npmjs.org/threads/-/threads-1.0.0-beta.9.tgz", + "integrity": "sha512-ZjpQvqA78p+y4jtlhnQsKc8V9AwUvrWwOhy9FkFKWO24JHKte3oWllmjvUw896YqrZymsJvqJwlbUHV1CpVtKw==", + "requires": { + "@types/zen-observable": "^0.8.0", + "callsites": "^3.1.0", + "debug": "^4.1.1", + "is-observable": "^1.1.0", + "observable-fns": "^0.4.0", + "tiny-worker": ">= 2", + "zen-observable": "^0.8.14" + } + }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -1160,6 +1207,14 @@ "xtend": "~4.0.0" } }, + "tiny-worker": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tiny-worker/-/tiny-worker-2.3.0.tgz", + "integrity": "sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==", + "requires": { + "esm": "^3.2.25" + } + }, "to-absolute-glob": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", @@ -1282,6 +1337,11 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" } } } diff --git a/plejd/package.json b/plejd/package.json index 3c177f8..09b1f77 100644 --- a/plejd/package.json +++ b/plejd/package.json @@ -1,7 +1,7 @@ { "dependencies": { "@abandonware/bluetooth-hci-socket": "0.5.3-3", - "@abandonware/noble": "^1.9.2-5", + "@icanos/noble": "^1.9.2-6", "axios": "^0.19.0", "buffer-xor": "^2.0.2", "fs": "0.0.1-security", @@ -10,4 +10,4 @@ "mqtt": "^3.0.0", "sleep": "^6.1.0" } -} +} \ No newline at end of file diff --git a/plejd/test/test.ble.js b/plejd/test/test.ble.js new file mode 100644 index 0000000..083360f --- /dev/null +++ b/plejd/test/test.ble.js @@ -0,0 +1,8 @@ +const PlejdService = require('../ble'); + +const plejd = new PlejdService('todo-insert-crypto-key', true); +plejd.on('authenticated', () => { + plejd.disconnect(); + console.log('ok, done! disconnected.'); +}); +plejd.scan(); \ No newline at end of file