From 9a84afada010908b91d0e43b5d0014f061e0abe5 Mon Sep 17 00:00:00 2001 From: Marcus Westin Date: Sun, 15 Dec 2019 16:34:07 +0100 Subject: [PATCH 1/9] rewriting the ble part --- plejd/ble.js | 339 ++++++++++++++++++++++++++++++++++++++++ plejd/package-lock.json | 8 +- plejd/package.json | 2 +- 3 files changed, 344 insertions(+), 5 deletions(-) create mode 100644 plejd/ble.js diff --git a/plejd/ble.js b/plejd/ble.js new file mode 100644 index 0000000..34701ed --- /dev/null +++ b/plejd/ble.js @@ -0,0 +1,339 @@ +const noble = require('@abandonware/noble'); +const crypto = require('crypto'); +const xor = require('buffer-xor'); +const _ = require('lodash'); +const EventEmitter = require('events'); +const sleep = require('sleep'); + +let debug = 'console'; + +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; + + // Holds a reference to all characteristics + this.characteristicState = STATE_UNINITIALIZED; + this.characteristics = { + data: null, + lastData: null, + auth: null, + ping: null + }; + + this._wireEvents(); + } + + scan() { + logger('scan()'); + + if (this.state === STATE_SCANNING) { + console.log('error: already scanning, please wait.'); + return; + } + + this.state = STATE_SCANNING; + noble.startScanning(); + + setTimeout(() => { + noble.stopScanning(); + this.state = STATE_IDLE; + + this.devices.sort((a, b) => (a.rssi > b.rssi) ? 1 : -1) + this.emit('scanComplete', this.devices); + }, 5000); + } + + connect(uuid = null) { + if (!uuid) { + this.device = Object.values(this.devices)[0]; + } + else { + this.device = this.devices[uuid]; + if (!this.device) { + console.log('error: could not find a device with uuid: ' + uuid); + return; + } + } + + this.device.connect(this.onDeviceConnected); + } + + 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'); + }); + }) + }); + } + + 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 { + 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.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.pingCharacteristic.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; + + this.state = STATE_CONNECTED; + + if (this.characteristicState === STATE_UNINITIALIZED) { + // We need to discover the characteristics + this.device.discoverSomeServicesAndCharacteristics([PLEJD_SERVICE], [], async (err, services, characteristics) => { + if (err) { + console.log('error: failed to discover services: ' + err); + return; + } + + 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.dataCharacteristic + && self.lastDataCharacteristic + && self.authCharacteristic + && self.pingCharacteristic) { + + self.characteristicState = STATE_INITIALIZED; + self.emit('deviceCharacteristicsComplete', self.device); + } + }); + } + } + + onDeviceCharacteristicsComplete(device) { + logger('onDeviceCharacteristicsComplete(' + device.id + ')'); + this.authenticate(); + } + + onDeviceDiscovered(device) { + logger('onDeviceDiscovered(' + device.id + ')'); + 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()'); + } + + onInterfaceStateChanged(state) { + logger('onInterfaceStateChanged(' + state + ')'); + + if (state === 'poweredOn') { + this.scan(); + } + } + + wireEvents() { + noble.on('stateChanged', this.onInterfaceStateChanged); + noble.on('scanStop', this.onDeviceScanComplete); + noble.on('discover', this.onDeviceDiscovered); + noble.on('disconnect', this.onDeviceDisconnected); + + this.on('deviceCharacteristicsComplete', this.onDeviceCharacteristicsComplete); + this.on('authenticated', this.onAuthenticated); + this.on('pingFailed', this.onPingFailed); + this.on('pingSuccess', this.onPingSuccess); + } + + _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 + } +} \ No newline at end of file diff --git a/plejd/package-lock.json b/plejd/package-lock.json index 711984e..7c8ec8a 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", diff --git a/plejd/package.json b/plejd/package.json index 3c177f8..fbf3ece 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", From 8ed90c1668488fcdb0c148a4414204ea4454b068 Mon Sep 17 00:00:00 2001 From: Marcus Westin Date: Sun, 15 Dec 2019 17:42:57 +0100 Subject: [PATCH 2/9] fixes --- plejd/ble.js | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/plejd/ble.js b/plejd/ble.js index 34701ed..7845910 100644 --- a/plejd/ble.js +++ b/plejd/ble.js @@ -1,4 +1,4 @@ -const noble = require('@abandonware/noble'); +const noble = require('@icanos/noble'); const crypto = require('crypto'); const xor = require('buffer-xor'); const _ = require('lodash'); @@ -57,7 +57,7 @@ class PlejdService extends EventEmitter { ping: null }; - this._wireEvents(); + this.wireEvents(); } scan() { @@ -81,6 +81,11 @@ class PlejdService extends EventEmitter { } connect(uuid = null) { + if (this.state === STATE_CONNECTING) { + console.log('warning: currently connecting to a device, please wait...'); + return; + } + if (!uuid) { this.device = Object.values(this.devices)[0]; } @@ -92,9 +97,21 @@ class PlejdService extends EventEmitter { } } + logger('connecting to ' + this.device.id + ' with addr ' + this.device.address + ' and rssi ' + this.device.rssi); + + this.state = STATE_CONNECTING; this.device.connect(this.onDeviceConnected); } + disconnect() { + logger('disconnect()'); + if (this.state !== STATE_CONNECTED) { + return; + } + + this.device.disconnect(); + } + authenticate() { logger('authenticate()'); const self = this; @@ -336,4 +353,6 @@ class PlejdService extends EventEmitter { return buffer } -} \ No newline at end of file +} + +module.exports = PlejdService; \ No newline at end of file From 1508df75a007491a564e1dff3a8583cff923abb4 Mon Sep 17 00:00:00 2001 From: Marcus Westin Date: Sun, 15 Dec 2019 17:44:33 +0100 Subject: [PATCH 3/9] added a test --- plejd/test/test.ble.js | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 plejd/test/test.ble.js 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 From 5d90bceb99131ec5e52b7f365027c635336343f5 Mon Sep 17 00:00:00 2001 From: icanos Date: Wed, 18 Dec 2019 22:12:57 +0000 Subject: [PATCH 4/9] improvements and added logging --- plejd/ble.js | 106 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 87 insertions(+), 19 deletions(-) diff --git a/plejd/ble.js b/plejd/ble.js index 7845910..9e386f5 100644 --- a/plejd/ble.js +++ b/plejd/ble.js @@ -47,6 +47,7 @@ class PlejdService extends EventEmitter { this.devices = {}; // Keeps track of the currently connected device this.device = null; + this.deviceIdx = 0; // Holds a reference to all characteristics this.characteristicState = STATE_UNINITIALIZED; @@ -57,6 +58,7 @@ class PlejdService extends EventEmitter { ping: null }; + logger('wiring events and waiting for BLE interface to power up.'); this.wireEvents(); } @@ -69,25 +71,33 @@ class PlejdService extends EventEmitter { } this.state = STATE_SCANNING; - noble.startScanning(); + noble.startScanning([PLEJD_SERVICE]); setTimeout(() => { noble.stopScanning(); this.state = STATE_IDLE; - this.devices.sort((a, b) => (a.rssi > b.rssi) ? 1 : -1) - this.emit('scanComplete', this.devices); + 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)[0]; + this.device = Object.values(this.devices)[this.deviceIdx]; } else { this.device = this.devices[uuid]; @@ -97,10 +107,32 @@ class PlejdService extends EventEmitter { } } + if (!this.device) { + console.log('error: reached end of device list. cannot continue.'); + return; + } + 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(this.onDeviceConnected); + this.device.connect((err) => { + self.onDeviceConnected(err); + }); + } + + reset() { + logger('reset()'); + this.state = STATE_IDLE; } disconnect() { @@ -201,7 +233,7 @@ class PlejdService extends EventEmitter { return; } - this.pingCharacteristic.read((err, data) => { + this.characteristics.ping.read((err, data) => { if (err) { console.log('error: unable to read ping: ' + err); self.emit('pingFailed'); @@ -228,16 +260,43 @@ class PlejdService extends EventEmitter { 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.'); @@ -257,10 +316,10 @@ class PlejdService extends EventEmitter { } }); - if (self.dataCharacteristic - && self.lastDataCharacteristic - && self.authCharacteristic - && self.pingCharacteristic) { + if (self.characteristics.data + && self.characteristics.lastData + && self.characteristics.auth + && self.characteristics.ping) { self.characteristicState = STATE_INITIALIZED; self.emit('deviceCharacteristicsComplete', self.device); @@ -276,7 +335,10 @@ class PlejdService extends EventEmitter { onDeviceDiscovered(device) { logger('onDeviceDiscovered(' + device.id + ')'); - this.devices[device.id] = device; + if (device.advertisement.localName === 'P mesh') { + logger('device is P mesh'); + this.devices[device.id] = device; + } } onDeviceDisconnected() { @@ -293,6 +355,8 @@ class PlejdService extends EventEmitter { onDeviceScanComplete() { logger('onDeviceScanComplete()'); + console.log('trying to connect to the mesh network.'); + this.connect(); } onInterfaceStateChanged(state) { @@ -304,15 +368,19 @@ class PlejdService extends EventEmitter { } wireEvents() { - noble.on('stateChanged', this.onInterfaceStateChanged); - noble.on('scanStop', this.onDeviceScanComplete); - noble.on('discover', this.onDeviceDiscovered); - noble.on('disconnect', this.onDeviceDisconnected); + logger('wireEvents()'); + const self = this; - this.on('deviceCharacteristicsComplete', this.onDeviceCharacteristicsComplete); - this.on('authenticated', this.onAuthenticated); - this.on('pingFailed', this.onPingFailed); - this.on('pingSuccess', this.onPingSuccess); + 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)); } _createChallengeResponse(key, challenge) { From c21d7e1243e938aa248957e1ce5afd52982e2538 Mon Sep 17 00:00:00 2001 From: icanos Date: Thu, 19 Dec 2019 09:41:16 +0000 Subject: [PATCH 5/9] impl turnon/off and notifications --- plejd/ble.js | 108 +++++++++++++++++++++++++++++++++++++++- plejd/package-lock.json | 60 ++++++++++++++++++++++ plejd/package.json | 2 +- 3 files changed, 167 insertions(+), 3 deletions(-) diff --git a/plejd/ble.js b/plejd/ble.js index 9e386f5..edfc739 100644 --- a/plejd/ble.js +++ b/plejd/ble.js @@ -3,7 +3,6 @@ const crypto = require('crypto'); const xor = require('buffer-xor'); const _ = require('lodash'); const EventEmitter = require('events'); -const sleep = require('sleep'); let debug = 'console'; @@ -47,8 +46,11 @@ class PlejdService extends EventEmitter { 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 = { @@ -62,6 +64,27 @@ class PlejdService extends EventEmitter { 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()'); @@ -72,7 +95,7 @@ class PlejdService extends EventEmitter { this.state = STATE_SCANNING; noble.startScanning([PLEJD_SERVICE]); - + setTimeout(() => { noble.stopScanning(); this.state = STATE_IDLE; @@ -112,6 +135,14 @@ class PlejdService extends EventEmitter { 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) { @@ -141,7 +172,12 @@ class PlejdService extends EventEmitter { return; } + clearInterval(this.pingRef); + + this.unsubscribeCharacteristics(); this.device.disconnect(); + + this.state = STATE_DISCONNECTED; } authenticate() { @@ -179,6 +215,22 @@ class PlejdService extends EventEmitter { }); } + 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()'); @@ -194,6 +246,9 @@ class PlejdService extends EventEmitter { 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.'); } @@ -210,7 +265,9 @@ class PlejdService extends EventEmitter { logger('stopping ping and reconnecting.'); clearInterval(this.pingRef); + this.unsubscribeCharacteristics(); this.state = STATE_DISCONNECTED; + this.connect(this.device.id); } @@ -322,6 +379,10 @@ class PlejdService extends EventEmitter { && self.characteristics.ping) { self.characteristicState = STATE_INITIALIZED; + + // subscribe to notifications + this.subscribeCharacteristics(); + self.emit('deviceCharacteristicsComplete', self.device); } }); @@ -367,6 +428,28 @@ class PlejdService extends EventEmitter { } } + 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; @@ -383,6 +466,27 @@ class PlejdService extends EventEmitter { 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); diff --git a/plejd/package-lock.json b/plejd/package-lock.json index 7c8ec8a..d1315fa 100644 --- a/plejd/package-lock.json +++ b/plejd/package-lock.json @@ -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 fbf3ece..09b1f77 100644 --- a/plejd/package.json +++ b/plejd/package.json @@ -10,4 +10,4 @@ "mqtt": "^3.0.0", "sleep": "^6.1.0" } -} +} \ No newline at end of file From 3c60835f1bdecf5f6596c69ee5e56138d8885d53 Mon Sep 17 00:00:00 2001 From: icanos Date: Thu, 19 Dec 2019 09:42:18 +0000 Subject: [PATCH 6/9] upped version --- plejd/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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/", From a27d7c11800e53f1d90ce738bd0fd0ee06b16550 Mon Sep 17 00:00:00 2001 From: icanos Date: Thu, 19 Dec 2019 09:54:37 +0000 Subject: [PATCH 7/9] muted logging --- plejd/ble.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plejd/ble.js b/plejd/ble.js index edfc739..1663822 100644 --- a/plejd/ble.js +++ b/plejd/ble.js @@ -4,7 +4,7 @@ const xor = require('buffer-xor'); const _ = require('lodash'); const EventEmitter = require('events'); -let debug = 'console'; +let debug = ''; const getLogger = () => { const consoleLogger = msg => console.log('plejd', msg); From 82c438409701e22f6d54d38f4b15a52995c9a456 Mon Sep 17 00:00:00 2001 From: icanos Date: Thu, 19 Dec 2019 09:58:53 +0000 Subject: [PATCH 8/9] changed ble adapter --- plejd/main.js | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) 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(); }); }); From ad7c1b5d92c2bd85871c654774499f90575dd0c7 Mon Sep 17 00:00:00 2001 From: Marcus Westin Date: Thu, 19 Dec 2019 21:25:40 +0100 Subject: [PATCH 9/9] fixed missing ref --- plejd/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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