From 7a1ee5a504b64ceed63b5d5765edae1cef0d02dc Mon Sep 17 00:00:00 2001 From: Marcus Westin Date: Fri, 17 Jan 2020 14:50:58 +0000 Subject: [PATCH 01/17] start converting ble to bluez --- .devcontainer/Dockerfile | 3 + plejd/ble.bluez.js | 766 +++++++++++++++++++++++++++++++++++++++ plejd/package-lock.json | 89 +---- plejd/package.json | 4 +- 4 files changed, 779 insertions(+), 83 deletions(-) create mode 100644 plejd/ble.bluez.js diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index bc00500..2ba13bc 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -22,6 +22,7 @@ RUN apt-get update \ # # Verify git and needed tools are installed && apt-get -y install git iproute2 procps \ + && apt-get -y install libdbus-1-dev libglib2.0-dev \ # # Remove outdated yarn from /opt and install via package # so it can be easily updated via apt-get upgrade yarn @@ -53,5 +54,7 @@ RUN apt-get update \ && apt-get clean -y \ && rm -rf /var/lib/apt/lists/* +RUN npm install -g node-gyp + # Switch back to dialog for any ad-hoc use of apt-get ENV DEBIAN_FRONTEND=dialog diff --git a/plejd/ble.bluez.js b/plejd/ble.bluez.js new file mode 100644 index 0000000..9238673 --- /dev/null +++ b/plejd/ble.bluez.js @@ -0,0 +1,766 @@ +const dbus = require('dbus'); +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'; + +const BLE_CMD_DIM_CHANGE = '00c8'; +const BLE_CMD_DIM2_CHANGE = '0098'; +const BLE_CMD_STATE_CHANGE = '0097'; +const BLE_CMD_SCENE_TRIG = '0021'; + +const BLUEZ_SERVICE_NAME = 'org.bluez'; +const DBUS_OBJECT_MANAGER = 'org.freedesktop.DBus.ObjectManager'; +const DBUS_PROPERTIES = 'org.freedesktop.DBus.Properties'; + +const BLUEZ_ADAPTER_ID = 'org.bluez.Adapter1'; +const BLUEZ_DEVICE_ID = 'org.bluez.Device1'; +const GATT_SERVICE_ID = 'org.bluez.GattService1'; +const GATT_CHRC_ID = 'org.bluez.GattCharacteristic1'; + +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 = []; + + this.plejdDevices = {}; + this.connectEventHooked = false; + + // Holds a reference to all characteristics + this.characteristics = { + data: null, + lastData: null, + auth: null, + ping: null + }; + + this.bus = dbus.registerService('system', null); + + logger('wiring events and waiting for BLE interface to power up.'); + this.wireEvents(); + } + + async init() { + if (this.objectManager) { + this.objectManager.removeAllListeners(); + } + + this.objectManager = await this.bus.getInterface(BLUEZ_SERVICE_NAME, '/', DBUS_OBJECT_MANAGER); + + this.objectManager.on('InterfacesAdded', this.onInterfacesAdded.bind(this)); + this.objectManager.on('InterfacesRemoved', this.onInterfacesRemoved.bind(this)); + this.objectManager.GetManagedObjects((err, objs) => { + Object.keys(objs).forEach((k) => this.onInterfacesAdded(k, objs[k])) + }); + } + + async onInterfacesAdded(path, interfaces) { + const [adapter, dev, service, characteristic] = path.split('/').slice(3); + + if ('org.bluez.Adapter1' in interfaces) { + console.log('has org.bluez.Adapter1'); + + const props = interfaces['org.bluez.Adapter1']; + console.log(props); + this.adapter_props = props; + } + + if ('org.bluez.Device1' in interfaces) { + console.log('has org.bluez.Device1'); + + const props = interfaces['org.bluez.Device1']; + console.log(props); + this.devices[props.Address] = path; + this.emit('device', props.Address, props); + } + + if ('org.bluez.GattService1' in interfaces) { + console.log('has org.bluez.GattService1'); + + const props = interfaces['org.bluez.GattService1']; + console.log(props); + + const iface = await this.getInterface('org.bluez', path, 'org.bluez.GattService1'); + const service = new Service(iface); + + const dev = await this.getDevice(props.Device); + + if (this._path[path]) { + // Characteristic was registered before service, copy them over + service.characteristics = this._path[path].characteristics || {}; + } + + dev.services[props.UUID] = service; + this._path[path] = service; + } + + if ('org.bluez.GattCharacteristic1' in interfaces) { + console.log('has org.bluez.GattCharacteristic1'); + const props = interfaces['org.bluez.GattCharacteristic1']; + console.log(props); + + const iface = await this.getInterface('org.bluez', path, 'org.bluez.GattCharacteristic1'); + const changed = await this.getInterface('org.bluez', path, 'org.freedesktop.DBus.Properties'); + const characteristic = new Characteristic(iface, changed); + + if (this._path[path]) { + // Descriptor was registered before characteristic, copy them over + characteristic.descriptors = this._path[path].descriptors || {}; + } + + if (!this._path[props.Service]) { + // service not jet created + this._path[props.Service] = { characteristics: {} }; + } + this._path[props.Service].characteristics[props.UUID] = characteristic; + this._path[path] = characteristic; + } + } + + async onInterfacesRemoved(path, interfaces) { + const [adapter, dev, service, characteristic] = path.split('/').slice(3); + } + + updateSettings(settings) { + if (settings.debug) { + debug = 'console'; + } + else { + debug = ''; + } + } + + turnOn(id, command) { + logger('turning on ' + id + ' at brightness ' + (!command.brightness ? 255 : command.brightness)); + const brightness = command.brightness ? command.brightness : 0; + + if (command.transition) { + // we have a transition time, split the target brightness + // into pieces spread of the transition time + const steps = command.transition * 2; + const brightnessStep = brightness / steps; + + let i = 0; + const transitionRef = setInterval(() => { + let currentBrightness = parseInt((brightnessStep * i) + 1); + if (currentBrightness > 254) { + currentBrightness = 254; + } + + this._turnOn(id, currentBrightness); + + if (i >= steps) { + clearInterval(transitionRef); + } + + i++; + }, 500); + } + else { + this._turnOn(id, brightness); + } + } + + _turnOn(id, brightness) { + var payload; + if (!brightness || brightness === 0) { + logger('no brightness specified, setting to previous known.'); + payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009701', 'hex'); + } else { + logger('brightness is ' + brightness); + 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, command) { + logger('turning off ' + id); + + if (command.transition) { + // we have a transition time, split the target brightness (which will be 0) + // into pieces spread of the transition time + const initialBrightness = this.plejdDevices[id] ? this.plejdDevices[id].dim : 250; + const steps = command.transition * 2; + const brightnessStep = initialBrightness / steps; + let currentBrightness = initialBrightness; + + let i = 0; + const transitionRef = setInterval(() => { + currentBrightness = parseInt(initialBrightness - (brightnessStep * i)); + if (currentBrightness <= 0 || i >= steps) { + clearInterval(transitionRef); + + // finally, we turn it off + this._turnOff(id); + return; + } + + this._turnOn(id, currentBrightness); + + i++; + }, 500); + } + else { + this._turnOff(id); + } + } + + _turnOff(id) { + var payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009700', 'hex'); + this.write(payload); + } + + scan() { + console.log('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; + // console.log('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' + // ) + // ); + + // console.log('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. cleaning up and trying next.'); + + // self.disconnect(); + + // self.deviceIdx++; + // self.connect(); + // } + // } + // }, 10 * 1000); + + // this.state = STATE_CONNECTING; + + // if (!this.connectEventHooked) { + // this.device.once('connect', (state) => { + // self.onDeviceConnected(state); + // this.connectEventHooked = false; + // }); + // this.connectEventHooked = true; + // } + + // this.device.connect(); + } + + disconnect() { + console.log('disconnect()'); + + clearInterval(this.pingRef); + + // if (this.device) { + // this.device.removeAllListeners('servicesDiscover'); + // this.device.removeAllListeners('connect'); + // this.device.removeAllListeners('disconnect'); + // } + // if (this.characteristics.auth) { + // this.characteristics.auth.removeAllListeners('read'); + // this.characteristics.auth.removeAllListeners('write'); + // } + // if (this.characteristics.data) { + // this.characteristics.data.removeAllListeners('read'); + // this.characteristics.data.removeAllListeners('write'); + // } + // if (this.characteristics.lastData) { + // this.characteristics.lastData.removeAllListeners('read'); + // this.characteristics.lastData.removeAllListeners('write'); + // } + // if (this.characteristics.ping) { + // this.characteristics.ping.removeAllListeners('read'); + // this.characteristics.ping.removeAllListeners('write'); + // } + + + this.connectEventHooked = false; + + // this.unsubscribeCharacteristics(); + // this.device.disconnect(); + + this.state = STATE_DISCONNECTED; + } + + authenticate() { + console.log('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; + } + + // try { + // 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); + // } + // } + // catch (error) { + // console.log('error: writing to plejd: ' + error); + // console.log('will reconnect and try again.'); + // this.writeQueue.push(data); + + // this.disconnect(); + // this.connect(); + // } + } + + onAuthenticated() { + // Start ping + logger('onAuthenticated()'); + this.startPing(); + } + + startPing() { + console.log('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('ping failed, reconnecting.'); + this.disconnect(); + this.connect(); + // clearInterval(this.pingRef); + + // this.unsubscribeCharacteristics(); + // this.state = STATE_DISCONNECTED; + // this.characteristicState = STATE_UNINITIALIZED; + + // 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) { + console.log('onDeviceConnected()'); + const self = this; + + // if (err) { + // console.log('error: failed to connect to device: ' + err + '. picking next.'); + // this.deviceIdx++; + // this.disconnect(); + // 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()'); + this.disconnect(); + + if (!this.device) { + console.log('warning: reconnect will not be performed.'); + return; + } + + // we just want to reconnect + // this.connect(this.device.id); + } + + onDeviceScanComplete() { + console.log('onDeviceScanComplete()'); + console.log('trying to connect to the mesh network.'); + this.connect(); + } + + // onInterfaceStateChanged(state) { + // console.log('onInterfaceStateChanged(' + state + ')'); + + // if (state === 'poweredOn') { + // this.scan(); + // } + // else { + // noble.stopScanning(); + // } + // } + + 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.length < 5) { + // ignore the notification since too small + return; + } + + const cmd = decoded.toString('hex', 3, 5); + + if (debug) { + logger('raw event received: ' + decoded.toString('hex')); + } + + if (cmd === BLE_CMD_DIM_CHANGE || cmd === BLE_CMD_DIM2_CHANGE) { + 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('stateChanged', device, { state: state, brightness: dim }); + } + else if (cmd === BLE_CMD_STATE_CHANGE) { + state = parseInt(decoded.toString('hex', 5, 6), 10); + + logger('d: ' + device + ' got state update: ' + state); + this.emit('stateChanged', device, { state: state }); + } + else if (cmd === BLE_CMD_SCENE_TRIG) { + const scene = parseInt(decoded.toString('hex', 5, 6), 10); + this.emit('sceneTriggered', device, scene); + } + + this.plejdDevices[device] = { + state: state, + dim: dim + }; + } + + wireEvents() { + console.log('wireEvents()'); + const self = this; + + // noble.on('stateChange', this.onInterfaceStateChanged.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) { + // try { + // this.characteristics.lastData.unsubscribe((err) => { + // if (err) { + // console.log('error: could not unsubscribe from event.'); + // } + // }); + // } + // catch (error) { + // console.log('warning: could not unsubscribe from lastData, probably already disconnected: ' + error); + // } + // } + } + + _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/package-lock.json b/plejd/package-lock.json index d1315fa..baa320c 100644 --- a/plejd/package-lock.json +++ b/plejd/package-lock.json @@ -13,22 +13,6 @@ "usb": "^1.6.0" } }, - "@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", - "napi-thread-safe-callback": "0.0.6", - "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", @@ -139,11 +123,6 @@ "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", @@ -205,6 +184,14 @@ "type": "^1.0.1" } }, + "dbus": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/dbus/-/dbus-1.0.5.tgz", + "integrity": "sha512-itMup/0lcjnwV4DULJtI8q37Nky713KE2b1QM4+W/0zlzAsMNJZURBtyENtnJc6BlHouqbur++PXWeEKDuGulg==", + "requires": { + "nan": "^2.13.2" + } + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -321,11 +308,6 @@ "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", @@ -565,14 +547,6 @@ "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", @@ -730,11 +704,6 @@ "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", @@ -769,11 +738,6 @@ "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", @@ -841,11 +805,6 @@ "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", @@ -1118,11 +1077,6 @@ "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", @@ -1175,20 +1129,6 @@ } } }, - "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", @@ -1207,14 +1147,6 @@ "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", @@ -1337,11 +1269,6 @@ "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 166653b..cf4a6c6 100644 --- a/plejd/package.json +++ b/plejd/package.json @@ -1,7 +1,7 @@ { "dependencies": { "@abandonware/bluetooth-hci-socket": "0.5.3-3", - "@icanos/noble": "^1.9.2-9", + "dbus": "^1.0.5", "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 +} From 18b94288e36beab1ef0bd210c4ec470ff32c48c8 Mon Sep 17 00:00:00 2001 From: Marcus Westin Date: Fri, 17 Jan 2020 15:00:54 +0000 Subject: [PATCH 02/17] further rewrites --- plejd/api.js | 13 +++++++------ plejd/main.js | 7 ++++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/plejd/api.js b/plejd/api.js index 91e2e98..0e41589 100644 --- a/plejd/api.js +++ b/plejd/api.js @@ -71,7 +71,7 @@ class PlejdApi extends EventEmitter { }) .catch((error) => { if (error.response.status === 400) { - console.log('error: server returned status 400. probably invalid credentials, please verify.'); + console.log('error: server returned status 400. probably invalid credentials, please verify.'); } else { console.log('error: unable to retrieve session token response: ' + error); @@ -100,10 +100,11 @@ class PlejdApi extends EventEmitter { self.site = response.data.result.find(x => x.site.title == self.siteName); self.cryptoKey = self.site.plejdMesh.cryptoKey; - callback(self.cryptoKey); + //callback(self.cryptoKey); + this.emit('ready', self.cryptoKey); }) .catch((error) => { - console.log('error: unable to retrieve the crypto key. error: ' + error + ' (code: ' + error.response.status + ')'); + console.log('error: unable to retrieve the crypto key. error: ' + error); return Promise.reject('unable to retrieve the crypto key. error: ' + error); }); } @@ -172,9 +173,9 @@ class PlejdApi extends EventEmitter { typeName: 'Room', dimmable: roomDevices[roomId].find(x => x.dimmable).length > 0 }; - + logger(JSON.stringify(newDevice)); - + devices.push(newDevice); } } @@ -204,7 +205,7 @@ class PlejdApi extends EventEmitter { // Unknown return { name: "-unknown-", type: 'light', dimmable: false }; case 10: - return { name: "-unknown-", type: 'light', dimmable: false }; + return { name: "-unknown-", type: 'light', dimmable: false }; case 12: // Unknown return { name: "-unknown-", type: 'light', dimmable: false }; diff --git a/plejd/main.js b/plejd/main.js index 5aff784..75b56d2 100644 --- a/plejd/main.js +++ b/plejd/main.js @@ -1,21 +1,22 @@ const api = require('./api'); const mqtt = require('./mqtt'); const fs = require('fs'); -const PlejdService = require('./ble'); +const PlejdService = require('./ble.bluez'); const version = "0.2.8"; async function main() { console.log('starting Plejd add-on v. ' + version); - const rawData = fs.readFileSync('/data/plejd.json'); + const rawData = fs.readFileSync('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) => { + plejdApi.getCryptoKey();//(cryptoKey) => { + plejdApi.on('ready', (cryptoKey) => { const devices = plejdApi.getDevices(); client.on('connected', () => { From ff04e8da41d059c33464e6f9602adf1eded5d50f Mon Sep 17 00:00:00 2001 From: icanos Date: Sat, 18 Jan 2020 15:50:46 +0000 Subject: [PATCH 03/17] continued work with bluez --- plejd/ble.bluez.js | 18 +++--- plejd/main.js | 2 + plejd/package-lock.json | 117 +++++++++++++++++++++++++++++++++++ plejd/package.json | 3 +- plejd/test/test.ble.bluez.js | 6 ++ 5 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 plejd/test/test.ble.bluez.js diff --git a/plejd/ble.bluez.js b/plejd/ble.bluez.js index 9238673..34513c3 100644 --- a/plejd/ble.bluez.js +++ b/plejd/ble.bluez.js @@ -1,4 +1,4 @@ -const dbus = require('dbus'); +const dbus = require('dbus-next'); const crypto = require('crypto'); const xor = require('buffer-xor'); const _ = require('lodash'); @@ -40,8 +40,8 @@ const BLE_CMD_STATE_CHANGE = '0097'; const BLE_CMD_SCENE_TRIG = '0021'; const BLUEZ_SERVICE_NAME = 'org.bluez'; -const DBUS_OBJECT_MANAGER = 'org.freedesktop.DBus.ObjectManager'; -const DBUS_PROPERTIES = 'org.freedesktop.DBus.Properties'; +const DBUS_OM_INTERFACE = 'org.freedesktop.DBus.ObjectManager'; +const DBUS_PROP_INTERFACE = 'org.freedesktop.DBus.Properties'; const BLUEZ_ADAPTER_ID = 'org.bluez.Adapter1'; const BLUEZ_DEVICE_ID = 'org.bluez.Device1'; @@ -76,7 +76,7 @@ class PlejdService extends EventEmitter { ping: null }; - this.bus = dbus.registerService('system', null); + this.bus = dbus.systemBus(); logger('wiring events and waiting for BLE interface to power up.'); this.wireEvents(); @@ -87,13 +87,11 @@ class PlejdService extends EventEmitter { this.objectManager.removeAllListeners(); } - this.objectManager = await this.bus.getInterface(BLUEZ_SERVICE_NAME, '/', DBUS_OBJECT_MANAGER); + const bluez = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, '/'); + this.objectManager = await bluez.getInterface(DBUS_OM_INTERFACE); - this.objectManager.on('InterfacesAdded', this.onInterfacesAdded.bind(this)); - this.objectManager.on('InterfacesRemoved', this.onInterfacesRemoved.bind(this)); - this.objectManager.GetManagedObjects((err, objs) => { - Object.keys(objs).forEach((k) => this.onInterfacesAdded(k, objs[k])) - }); + let objs = await this.objectManager.GetManagedObjects(); + console.log(objs); } async onInterfacesAdded(path, interfaces) { diff --git a/plejd/main.js b/plejd/main.js index 75b56d2..aa6a678 100644 --- a/plejd/main.js +++ b/plejd/main.js @@ -28,6 +28,8 @@ async function main() { // init the BLE interface const plejd = new PlejdService(cryptoKey, true); + plejd.init(); + plejd.on('authenticated', () => { console.log('plejd: connected via bluetooth.'); }); diff --git a/plejd/package-lock.json b/plejd/package-lock.json index baa320c..4478971 100644 --- a/plejd/package-lock.json +++ b/plejd/package-lock.json @@ -13,11 +13,26 @@ "usb": "^1.6.0" } }, + "@nornagon/put": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@nornagon/put/-/put-0.0.8.tgz", + "integrity": "sha512-ugvXJjwF5ldtUpa7D95kruNJ41yFQDEKyF5CW4TgKJnh+W/zmlBzXXeKTyqIgwMFrkePN2JqOBqcF0M0oOunow==" + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "abstract-socket": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/abstract-socket/-/abstract-socket-2.1.1.tgz", + "integrity": "sha512-YZJizsvS1aBua5Gd01woe4zuyYBGgSMeqDOB6/ChwdTI904KP6QGtJswXl4hcqWxbz86hQBe++HWV0hF1aGUtA==", + "optional": true, + "requires": { + "bindings": "^1.2.1", + "nan": "^2.12.1" + } + }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -192,6 +207,21 @@ "nan": "^2.13.2" } }, + "dbus-next": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/dbus-next/-/dbus-next-0.8.1.tgz", + "integrity": "sha512-/sgwpcnCkLQuMOTF9I95x6qvJZCbTK2RAbHwh7C60VMroSU7rphydj3ujpqiSy5yq04aKc3meZIHpCOrZ2791g==", + "requires": { + "@nornagon/put": "0.0.8", + "abstract-socket": "^2.0.0", + "event-stream": "3.3.4", + "hexy": "^0.2.10", + "jsbi": "^2.0.5", + "long": "^4.0.0", + "safe-buffer": "^5.1.1", + "xml2js": "^0.4.17" + } + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -224,6 +254,11 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" + }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -317,6 +352,20 @@ "es5-ext": "~0.10.14" } }, + "event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", + "requires": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, "expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -372,6 +421,11 @@ } } }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=" + }, "fs": { "version": "0.0.1-security", "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", @@ -472,6 +526,11 @@ "xtend": "^4.0.0" } }, + "hexy": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/hexy/-/hexy-0.2.11.tgz", + "integrity": "sha512-ciq6hFsSG/Bpt2DmrZJtv+56zpPdnq+NQ4ijEFrveKN0ZG1mhl/LdT1NQZ9se6ty1fACcI4d4vYqC9v8EYpH2A==" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -573,6 +632,11 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "jsbi": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-2.0.5.tgz", + "integrity": "sha512-TzO/62Hxeb26QMb4IGlI/5X+QLr9Uqp1FPkwp2+KOICW+Q+vSuFj61c8pkT6wAns4WcK56X7CmSHhJeDGWOqxQ==" + }, "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", @@ -593,6 +657,16 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=" + }, "mimic-response": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.0.0.tgz", @@ -850,6 +924,14 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "requires": { + "through": "~2.3" + } + }, "prebuild-install": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.3.tgz", @@ -1021,6 +1103,14 @@ "nan": "^2.13.2" } }, + "split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "requires": { + "through": "2" + } + }, "split2": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/split2/-/split2-3.1.1.tgz", @@ -1041,6 +1131,14 @@ } } }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "requires": { + "duplexer": "~0.1.1" + } + }, "stream-shift": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", @@ -1129,6 +1227,11 @@ } } }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -1260,6 +1363,20 @@ "ultron": "~1.1.0" } }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/plejd/package.json b/plejd/package.json index cf4a6c6..9e21fea 100644 --- a/plejd/package.json +++ b/plejd/package.json @@ -1,9 +1,10 @@ { "dependencies": { "@abandonware/bluetooth-hci-socket": "0.5.3-3", - "dbus": "^1.0.5", "axios": "^0.19.0", "buffer-xor": "^2.0.2", + "dbus": "^1.0.5", + "dbus-next": "^0.8.1", "fs": "0.0.1-security", "jspack": "0.0.4", "lodash": "^4.17.15", diff --git a/plejd/test/test.ble.bluez.js b/plejd/test/test.ble.bluez.js new file mode 100644 index 0000000..d4cf13c --- /dev/null +++ b/plejd/test/test.ble.bluez.js @@ -0,0 +1,6 @@ +const PlejdService = require('../ble.bluez'); + +const cryptoKey = ''; + +const plejd = new PlejdService(cryptoKey, true); +plejd.init(); \ No newline at end of file From 682a181a281ab8a80f222c16046dadb75852e51e Mon Sep 17 00:00:00 2001 From: icanos Date: Sun, 19 Jan 2020 20:08:48 +0000 Subject: [PATCH 04/17] continued work on bluez implementation --- plejd/ble.bluez.js | 692 +++++++++++++++++---------------------------- 1 file changed, 263 insertions(+), 429 deletions(-) diff --git a/plejd/ble.bluez.js b/plejd/ble.bluez.js index 34513c3..79ad40d 100644 --- a/plejd/ble.bluez.js +++ b/plejd/ble.bluez.js @@ -3,11 +3,12 @@ const crypto = require('crypto'); const xor = require('buffer-xor'); const _ = require('lodash'); const EventEmitter = require('events'); +const sleep = require('sleep'); -let debug = ''; +let debug = 'console'; const getLogger = () => { - const consoleLogger = msg => console.log('plejd', msg); + const consoleLogger = msg => console.log('plejd-ble', msg); if (debug === 'console') { return consoleLogger; } @@ -19,11 +20,11 @@ const getLogger = () => { 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 PLEJD_SERVICE = '31ba0001-6085-4726-be45-040c957391b5'; +const DATA_UUID = '31ba0004-6085-4726-be45-040c957391b5'; +const LAST_DATA_UUID = '31ba0005-6085-4726-be45-040c957391b5'; +const AUTH_UUID = '31ba0009-6085-4726-be45-040c957391b5'; +const PING_UUID = '31ba000a-6085-4726-be45-040c957391b5'; const STATE_IDLE = 'idle'; const STATE_SCANNING = 'scanning'; @@ -56,15 +57,10 @@ class PlejdService extends EventEmitter { // 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 = []; + this.plejdService = null; + this.bleDevices = []; this.plejdDevices = {}; this.connectEventHooked = false; @@ -72,11 +68,13 @@ class PlejdService extends EventEmitter { this.characteristics = { data: null, lastData: null, + lastDataProperties: null, auth: null, ping: null }; this.bus = dbus.systemBus(); + this.adapter = null; logger('wiring events and waiting for BLE interface to power up.'); this.wireEvents(); @@ -90,70 +88,126 @@ class PlejdService extends EventEmitter { const bluez = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, '/'); this.objectManager = await bluez.getInterface(DBUS_OM_INTERFACE); - let objs = await this.objectManager.GetManagedObjects(); - console.log(objs); + // We need to find the ble interface which implements the Adapter1 interface + const managedObjects = await this.objectManager.GetManagedObjects(); + let result = await this._getInterface(managedObjects, BLUEZ_ADAPTER_ID); + + if (result) { + this.adapter = result[1]; + } + + if (!this.adapter) { + console.log('plejd-ble: error: unable to find a bluetooth adapter that is compatible.'); + return; + } + + for (let path of Object.keys(managedObjects)) { + const interfaces = Object.keys(managedObjects[path]); + + if (interfaces.indexOf(BLUEZ_DEVICE_ID) > -1) { + const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path); + const device = await proxyObject.getInterface(BLUEZ_DEVICE_ID); + + const connected = managedObjects[path][BLUEZ_DEVICE_ID].Connected.value; + + if (connected) { + console.log('plejd-ble: disconnecting ' + path); + await device.Disconnect(); + } + + await this.adapter.RemoveDevice(path); + } + } + + this.objectManager.on('InterfacesAdded', this.onInterfacesAdded.bind(this)); + this.objectManager.on('InterfacesRemoved', this.onInterfacesRemoved.bind(this)); + + this.adapter.SetDiscoveryFilter({ + 'UUIDs': new dbus.Variant('as', [PLEJD_SERVICE]), + 'Transport': new dbus.Variant('s', 'le') + }); + + await this.adapter.StartDiscovery(); + + setTimeout(async () => { + await this._internalInit(); + }, 2000); + } + + async _internalInit() { + console.log('plejd-ble: got ' + this.bleDevices.length + ' device(s).'); + + for (const plejd of this.bleDevices) { + console.log('inspecting ' + plejd['path']); + + try { + const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, plejd['path']); + const device = await proxyObject.getInterface(BLUEZ_DEVICE_ID); + const properties = await proxyObject.getInterface(DBUS_PROP_INTERFACE); + + plejd['rssi'] = (await properties.Get(BLUEZ_DEVICE_ID, 'RSSI')).value; + plejd['instance'] = device; + + console.log('plejd-ble: discovered ' + plejd['path'] + ' with rssi ' + plejd['rssi']); + } + catch (err) { + console.log('plejd-ble: failed inspecting ' + plejd['path'] + ' error: ' + err); + } + } + + const sortedDevices = this.bleDevices.sort((a, b) => b['rssi'] - a['rssi']); + let connectedDevice = null; + + for (const plejd of sortedDevices) { + try { + logger('connecting to ' + plejd['path']); + await plejd['instance'].Connect(); + connectedDevice = plejd; + break + } + catch (err) { + console.log('plejd-ble: failed connecting to plejd, error: ' + err); + } + } + + setTimeout(async () => { + await this.onDeviceConnected(connectedDevice); + + await this.adapter.StopDiscovery(); + }, 2000); + } + + async _getInterface(managedObjects, iface) { + const managedPaths = Object.keys(managedObjects); + + for (let path of managedPaths) { + const pathInterfaces = Object.keys(managedObjects[path]); + if (pathInterfaces.indexOf(iface) > -1) { + console.log('plejd-ble: found ble interface \'' + iface + '\' at ' + path); + try { + const adapterObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path); + return [path, adapterObject.getInterface(iface), adapterObject]; + } + catch (err) { + console.log('plejd-ble: error: failed to get interface \'' + iface + '\': ' + err); + } + } + } + + return null; } async onInterfacesAdded(path, interfaces) { const [adapter, dev, service, characteristic] = path.split('/').slice(3); + const interfaceKeys = Object.keys(interfaces); - if ('org.bluez.Adapter1' in interfaces) { - console.log('has org.bluez.Adapter1'); - - const props = interfaces['org.bluez.Adapter1']; - console.log(props); - this.adapter_props = props; - } - - if ('org.bluez.Device1' in interfaces) { - console.log('has org.bluez.Device1'); - - const props = interfaces['org.bluez.Device1']; - console.log(props); - this.devices[props.Address] = path; - this.emit('device', props.Address, props); - } - - if ('org.bluez.GattService1' in interfaces) { - console.log('has org.bluez.GattService1'); - - const props = interfaces['org.bluez.GattService1']; - console.log(props); - - const iface = await this.getInterface('org.bluez', path, 'org.bluez.GattService1'); - const service = new Service(iface); - - const dev = await this.getDevice(props.Device); - - if (this._path[path]) { - // Characteristic was registered before service, copy them over - service.characteristics = this._path[path].characteristics || {}; + if (interfaceKeys.indexOf(BLUEZ_DEVICE_ID) > -1) { + if (interfaces[BLUEZ_DEVICE_ID]['UUIDs'].value.indexOf(PLEJD_SERVICE) > -1) { + console.log('found ' + path); + this.bleDevices.push({ 'path': path }); + } else { + console.log('uh oh, no Plejd device.'); } - - dev.services[props.UUID] = service; - this._path[path] = service; - } - - if ('org.bluez.GattCharacteristic1' in interfaces) { - console.log('has org.bluez.GattCharacteristic1'); - const props = interfaces['org.bluez.GattCharacteristic1']; - console.log(props); - - const iface = await this.getInterface('org.bluez', path, 'org.bluez.GattCharacteristic1'); - const changed = await this.getInterface('org.bluez', path, 'org.freedesktop.DBus.Properties'); - const characteristic = new Characteristic(iface, changed); - - if (this._path[path]) { - // Descriptor was registered before characteristic, copy them over - characteristic.descriptors = this._path[path].descriptors || {}; - } - - if (!this._path[props.Service]) { - // service not jet created - this._path[props.Service] = { characteristics: {} }; - } - this._path[props.Service].characteristics[props.UUID] = characteristic; - this._path[path] = characteristic; } } @@ -252,186 +306,45 @@ class PlejdService extends EventEmitter { this.write(payload); } - scan() { - console.log('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; - // console.log('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' - // ) - // ); - - // console.log('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. cleaning up and trying next.'); - - // self.disconnect(); - - // self.deviceIdx++; - // self.connect(); - // } - // } - // }, 10 * 1000); - - // this.state = STATE_CONNECTING; - - // if (!this.connectEventHooked) { - // this.device.once('connect', (state) => { - // self.onDeviceConnected(state); - // this.connectEventHooked = false; - // }); - // this.connectEventHooked = true; - // } - - // this.device.connect(); - } - disconnect() { console.log('disconnect()'); clearInterval(this.pingRef); - // if (this.device) { - // this.device.removeAllListeners('servicesDiscover'); - // this.device.removeAllListeners('connect'); - // this.device.removeAllListeners('disconnect'); - // } - // if (this.characteristics.auth) { - // this.characteristics.auth.removeAllListeners('read'); - // this.characteristics.auth.removeAllListeners('write'); - // } - // if (this.characteristics.data) { - // this.characteristics.data.removeAllListeners('read'); - // this.characteristics.data.removeAllListeners('write'); - // } - // if (this.characteristics.lastData) { - // this.characteristics.lastData.removeAllListeners('read'); - // this.characteristics.lastData.removeAllListeners('write'); - // } - // if (this.characteristics.ping) { - // this.characteristics.ping.removeAllListeners('read'); - // this.characteristics.ping.removeAllListeners('write'); - // } - - - this.connectEventHooked = false; - - // this.unsubscribeCharacteristics(); - // this.device.disconnect(); - - this.state = STATE_DISCONNECTED; } - authenticate() { + async authenticate() { console.log('authenticate()'); const self = this; - if (this.state !== STATE_CONNECTED) { - console.log('error: need to be connected and not previously authenticated (new connection).'); - return; + try { + console.log('sending challenge'); + await this.characteristics.auth.WriteValue([0], {}); + console.log('reading response'); + const challenge = await this.characteristics.auth.ReadValue({}); + const response = this._createChallengeResponse(this.cryptoKey, challenge); + await this.characteristics.auth.WriteValue([...response], {}); + + this.emit('authenticated'); + } + catch (err) { + console.log('plejd-ble: error: failed to authenticate: ' + err); } - - // 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; + async write(data, retry = true) { + try { + const encryptedData = this._encryptDecrypt(this.cryptoKey, this.plejdService.addr, data); + await this.characteristics.data.WriteValue([...encryptedData], {}); } + catch (err) { + console.log('plejd-ble: write failed ' + err); + await this.init(); - // try { - // 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); - // } - // } - // catch (error) { - // console.log('error: writing to plejd: ' + error); - // console.log('will reconnect and try again.'); - // this.writeQueue.push(data); - - // this.disconnect(); - // this.connect(); - // } + if (retry) { + await this.write(data, false); + } + } } onAuthenticated() { @@ -440,21 +353,13 @@ class PlejdService extends EventEmitter { this.startPing(); } - startPing() { + async startPing() { console.log('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.'); - } + logger('ping'); + await this.ping(); }, 3000); } @@ -462,184 +367,144 @@ class PlejdService extends EventEmitter { logger('pong: ' + nr); } - onPingFailed(error) { + async onPingFailed(error) { logger('onPingFailed(' + error + ')'); + console.log('plejd-ble: ping failed, reconnecting.'); - logger('ping failed, reconnecting.'); - this.disconnect(); - this.connect(); - // clearInterval(this.pingRef); - - // this.unsubscribeCharacteristics(); - // this.state = STATE_DISCONNECTED; - // this.characteristicState = STATE_UNINITIALIZED; - - // this.connect(this.device.id); + clearInterval(this.pingRef); + await this.init(); } - ping() { + async ping() { logger('ping()'); - if (this.state !== STATE_AUTHENTICATED) { - console.log('error: needs to be authenticated before pinging.'); + var ping = crypto.randomBytes(1); + let pong = null; + + try { + await this.characteristics.ping.WriteValue([...ping], {}); + pong = await this.characteristics.ping.ReadValue({}); + } + catch (err) { + console.log('error: writing to plejd: ' + err); + this.emit('pingFailed', 'write error'); return; } - const self = this; - var ping = crypto.randomBytes(1); + if (((ping[0] + 1) & 0xff) !== pong[0]) { + console.log('error: plejd ping failed'); + this.emit('pingFailed', 'plejd ping failed ' + ping[0] + ' - ' + pong[0]); + return; + } - // 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); - // } + this.emit('pingSuccess', pong[0]); } - onDeviceConnected(err) { + async _processPlejdService(path, characteristics) { + const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path); + const service = await proxyObject.getInterface(GATT_SERVICE_ID); + const properties = await proxyObject.getInterface(DBUS_PROP_INTERFACE); + + const uuid = (await properties.Get(GATT_SERVICE_ID, 'UUID')).value; + if (uuid !== PLEJD_SERVICE) { + console.log('plejd-ble: not a Plejd device.'); + return null; + } + + const dev = (await properties.Get(GATT_SERVICE_ID, 'Device')).value; + const regex = /dev_([0-9A-F_]+)$/; + const dirtyAddr = regex.exec(dev); + const addr = this._reverseBuffer( + Buffer.from( + String(dirtyAddr[1]) + .replace(/\-/g, '') + .replace(/\:/g, ''), 'hex' + ) + ); + + for (const chPath of characteristics) { + const chProxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, chPath); + const ch = await chProxyObject.getInterface(GATT_CHRC_ID); + const prop = await chProxyObject.getInterface(DBUS_PROP_INTERFACE); + + const chUuid = (await prop.Get(GATT_CHRC_ID, 'UUID')).value; + + if (chUuid === DATA_UUID) { + logger('found DATA characteristic.'); + this.characteristics.data = ch; + } + else if (chUuid === LAST_DATA_UUID) { + logger('found LAST_DATA characteristic.'); + this.characteristics.lastData = ch; + this.characteristics.lastDataProperties = prop; + } + else if (chUuid === AUTH_UUID) { + logger('found AUTH characteristic.'); + this.characteristics.auth = ch; + } + else if (chUuid === PING_UUID) { + logger('found PING characteristic.'); + this.characteristics.ping = ch; + } + } + + return { + addr: [...addr] + }; + } + + async onDeviceConnected(device) { console.log('onDeviceConnected()'); - const self = this; - // if (err) { - // console.log('error: failed to connect to device: ' + err + '. picking next.'); - // this.deviceIdx++; - // this.disconnect(); - // this.connect(); - // return; - // } + const objects = await this.objectManager.GetManagedObjects(); + let characteristics = []; - // this.state = STATE_CONNECTED; + console.log(objects); - // if (this.characteristicState === STATE_UNINITIALIZED) { - // // We need to discover the characteristics - // logger('discovering services and characteristics'); + for (const path of Object.keys(objects)) { + const interfaces = Object.keys(objects[path]); + if (interfaces.indexOf(GATT_CHRC_ID) > -1) { + characteristics.push(path); + } + } - // setTimeout(() => { - // if (this.characteristicState === STATE_UNINITIALIZED) { - // console.log('error: discovering characteristics timed out. trying next device.'); - // self.deviceIdx++; - // self.disconnect(); - // self.connect(); - // } - // }, 5000); + for (const path of Object.keys(objects)) { + const interfaces = Object.keys(objects[path]); + console.log(interfaces); + if (interfaces.indexOf(GATT_SERVICE_ID) > -1) { + let chPaths = []; + for (const c of characteristics) { + if (c[0] === '/') { + chPaths.push(c); + } + } - // this.device.discoverSomeServicesAndCharacteristics([PLEJD_SERVICE], [], async (err, services, characteristics) => { - // if (err) { - // console.log('error: failed to discover services: ' + err); - // return; - // } + console.log('trying ' + chPaths.length + ' characteristics'); - // 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; - // } + this.plejdService = await this._processPlejdService(path, chPaths); + if (this.plejdService) { + break; + } + } + } - // logger('found ' + characteristics.length + ' characteristic(s).'); + console.log(this.plejdService); - // 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 (!this.plejdService) { + console.log('plejd-ble: error: unable to connect to the Plejd mesh.'); + return; + } - // 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); - // } - // }); - // } + this.emit('deviceCharacteristicsComplete'); } - onDeviceCharacteristicsComplete(device) { - logger('onDeviceCharacteristicsComplete(' + device.id + ')'); + onDeviceCharacteristicsComplete() { + logger('onDeviceCharacteristicsComplete()'); 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()'); - this.disconnect(); - - if (!this.device) { - console.log('warning: reconnect will not be performed.'); - return; - } - - // we just want to reconnect - // this.connect(this.device.id); - } - - onDeviceScanComplete() { - console.log('onDeviceScanComplete()'); - console.log('trying to connect to the mesh network.'); - this.connect(); - } - - // onInterfaceStateChanged(state) { - // console.log('onInterfaceStateChanged(' + state + ')'); - - // if (state === 'poweredOn') { - // this.scan(); - // } - // else { - // noble.stopScanning(); - // } - // } - onLastDataUpdated(data, isNotification) { - const decoded = this._encryptDecrypt(this.cryptoKey, this.deviceAddress, data); + const decoded = this._encryptDecrypt(this.cryptoKey, this.plejdService.addr, data); let state = 0; let dim = 0; @@ -684,43 +549,12 @@ class PlejdService extends EventEmitter { console.log('wireEvents()'); const self = this; - // noble.on('stateChange', this.onInterfaceStateChanged.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) { - // try { - // this.characteristics.lastData.unsubscribe((err) => { - // if (err) { - // console.log('error: could not unsubscribe from event.'); - // } - // }); - // } - // catch (error) { - // console.log('warning: could not unsubscribe from lastData, probably already disconnected: ' + error); - // } - // } - } - _createChallengeResponse(key, challenge) { const intermediate = crypto.createHash('sha256').update(xor(key, challenge)).digest(); const part1 = intermediate.subarray(0, 16); From 51c257d461629f4dd5fd78e56e284d14caee75d4 Mon Sep 17 00:00:00 2001 From: Marcus Westin Date: Mon, 20 Jan 2020 07:58:23 +0000 Subject: [PATCH 05/17] added event handler for lastData, cleaned up loggin --- plejd/ble.bluez.js | 62 +++++++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/plejd/ble.bluez.js b/plejd/ble.bluez.js index 79ad40d..f44edfe 100644 --- a/plejd/ble.bluez.js +++ b/plejd/ble.bluez.js @@ -26,15 +26,6 @@ const LAST_DATA_UUID = '31ba0005-6085-4726-be45-040c957391b5'; const AUTH_UUID = '31ba0009-6085-4726-be45-040c957391b5'; const PING_UUID = '31ba000a-6085-4726-be45-040c957391b5'; -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'; - const BLE_CMD_DIM_CHANGE = '00c8'; const BLE_CMD_DIM2_CHANGE = '0098'; const BLE_CMD_STATE_CHANGE = '0097'; @@ -55,10 +46,6 @@ class PlejdService extends EventEmitter { this.cryptoKey = Buffer.from(cryptoKey.replace(/-/g, ''), 'hex'); - // Keeps track of the current state - this.state = STATE_IDLE; - this.writeQueue = []; - this.plejdService = null; this.bleDevices = []; this.plejdDevices = {}; @@ -90,7 +77,7 @@ class PlejdService extends EventEmitter { // We need to find the ble interface which implements the Adapter1 interface const managedObjects = await this.objectManager.GetManagedObjects(); - let result = await this._getInterface(managedObjects, BLUEZ_ADAPTER_ID); + let result = await this._getInterface(managedObjects, BLUEZ_ADAPTER_ID); if (result) { this.adapter = result[1]; @@ -103,7 +90,7 @@ class PlejdService extends EventEmitter { for (let path of Object.keys(managedObjects)) { const interfaces = Object.keys(managedObjects[path]); - + if (interfaces.indexOf(BLUEZ_DEVICE_ID) > -1) { const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path); const device = await proxyObject.getInterface(BLUEZ_DEVICE_ID); @@ -128,27 +115,27 @@ class PlejdService extends EventEmitter { }); await this.adapter.StartDiscovery(); - + setTimeout(async () => { await this._internalInit(); }, 2000); } async _internalInit() { - console.log('plejd-ble: got ' + this.bleDevices.length + ' device(s).'); + logger('got ' + this.bleDevices.length + ' device(s).'); for (const plejd of this.bleDevices) { - console.log('inspecting ' + plejd['path']); + logger('inspecting ' + plejd['path']); try { const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, plejd['path']); const device = await proxyObject.getInterface(BLUEZ_DEVICE_ID); const properties = await proxyObject.getInterface(DBUS_PROP_INTERFACE); - + plejd['rssi'] = (await properties.Get(BLUEZ_DEVICE_ID, 'RSSI')).value; plejd['instance'] = device; - console.log('plejd-ble: discovered ' + plejd['path'] + ' with rssi ' + plejd['rssi']); + logger('discovered ' + plejd['path'] + ' with rssi ' + plejd['rssi']); } catch (err) { console.log('plejd-ble: failed inspecting ' + plejd['path'] + ' error: ' + err); @@ -160,7 +147,7 @@ class PlejdService extends EventEmitter { for (const plejd of sortedDevices) { try { - logger('connecting to ' + plejd['path']); + console.log('plejd-ble: connecting to ' + plejd['path']); await plejd['instance'].Connect(); connectedDevice = plejd; break @@ -183,7 +170,7 @@ class PlejdService extends EventEmitter { for (let path of managedPaths) { const pathInterfaces = Object.keys(managedObjects[path]); if (pathInterfaces.indexOf(iface) > -1) { - console.log('plejd-ble: found ble interface \'' + iface + '\' at ' + path); + logger('found ble interface \'' + iface + '\' at ' + path); try { const adapterObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path); return [path, adapterObject.getInterface(iface), adapterObject]; @@ -203,7 +190,7 @@ class PlejdService extends EventEmitter { if (interfaceKeys.indexOf(BLUEZ_DEVICE_ID) > -1) { if (interfaces[BLUEZ_DEVICE_ID]['UUIDs'].value.indexOf(PLEJD_SERVICE) > -1) { - console.log('found ' + path); + logger('found Plejd service on ' + path); this.bleDevices.push({ 'path': path }); } else { console.log('uh oh, no Plejd device.'); @@ -318,11 +305,12 @@ class PlejdService extends EventEmitter { const self = this; try { - console.log('sending challenge'); + logger('sending challenge to device'); await this.characteristics.auth.WriteValue([0], {}); - console.log('reading response'); + logger('reading response from device'); const challenge = await this.characteristics.auth.ReadValue({}); const response = this._createChallengeResponse(this.cryptoKey, challenge); + logger('responding to authenticate'); await this.characteristics.auth.WriteValue([...response], {}); this.emit('authenticated'); @@ -342,6 +330,7 @@ class PlejdService extends EventEmitter { await this.init(); if (retry) { + logger('reconnected and retrying to write'); await this.write(data, false); } } @@ -488,10 +477,9 @@ class PlejdService extends EventEmitter { } } - console.log(this.plejdService); - if (!this.plejdService) { console.log('plejd-ble: error: unable to connect to the Plejd mesh.'); + this.emit('connectFailed'); return; } @@ -501,9 +489,27 @@ class PlejdService extends EventEmitter { onDeviceCharacteristicsComplete() { logger('onDeviceCharacteristicsComplete()'); this.authenticate(); + + // After we've authenticated, we need to hook up the event listener + // for changes to lastData. + this.characteristics.lastData.on('PropertiesChanged', this.onLastDataUpdated.bind(this)); } - onLastDataUpdated(data, isNotification) { + //onLastDataUpdated(data, isNotification) { + onLastDataUpdated(iface, properties, invalidated) { + if (iface !== GATT_CHRC_ID) { + return; + } + const changedKeys = Object.keys(properties); + if (changedKeys.length === 0) { + return; + } + + console.log(properties); + + const value = await properties.Get('Value'); + const data = value.value; + return; const decoded = this._encryptDecrypt(this.cryptoKey, this.plejdService.addr, data); let state = 0; From 667beaa11fd6fec0b6f9aebd71b82433194b8ae7 Mon Sep 17 00:00:00 2001 From: icanos Date: Mon, 20 Jan 2020 10:58:03 +0000 Subject: [PATCH 06/17] continued work --- plejd/ble.bluez.js | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/plejd/ble.bluez.js b/plejd/ble.bluez.js index f44edfe..2e232a5 100644 --- a/plejd/ble.bluez.js +++ b/plejd/ble.bluez.js @@ -72,6 +72,8 @@ class PlejdService extends EventEmitter { this.objectManager.removeAllListeners(); } + clearInterval(this.pingRef); + const bluez = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, '/'); this.objectManager = await bluez.getInterface(DBUS_OM_INTERFACE); @@ -293,13 +295,6 @@ class PlejdService extends EventEmitter { this.write(payload); } - disconnect() { - console.log('disconnect()'); - - clearInterval(this.pingRef); - - } - async authenticate() { console.log('authenticate()'); const self = this; @@ -322,17 +317,20 @@ class PlejdService extends EventEmitter { async write(data, retry = true) { try { + console.log('plejd-ble: sending ' + data.length + ' byte(s) of data to Plejd'); const encryptedData = this._encryptDecrypt(this.cryptoKey, this.plejdService.addr, data); await this.characteristics.data.WriteValue([...encryptedData], {}); } catch (err) { console.log('plejd-ble: write failed ' + err); - await this.init(); + setTimeout(async () => { + await this.init(); - if (retry) { - logger('reconnected and retrying to write'); - await this.write(data, false); - } + if (retry) { + logger('reconnected and retrying to write'); + await this.write(data, false); + } + }, 2000); } } @@ -350,6 +348,8 @@ class PlejdService extends EventEmitter { logger('ping'); await this.ping(); }, 3000); + + await this.ping(); } onPingSuccess(nr) { @@ -407,6 +407,7 @@ class PlejdService extends EventEmitter { Buffer.from( String(dirtyAddr[1]) .replace(/\-/g, '') + .replace(/\_/g, '') .replace(/\:/g, ''), 'hex' ) ); @@ -438,7 +439,7 @@ class PlejdService extends EventEmitter { } return { - addr: [...addr] + addr: addr }; } @@ -448,8 +449,6 @@ class PlejdService extends EventEmitter { const objects = await this.objectManager.GetManagedObjects(); let characteristics = []; - console.log(objects); - for (const path of Object.keys(objects)) { const interfaces = Object.keys(objects[path]); if (interfaces.indexOf(GATT_CHRC_ID) > -1) { @@ -459,7 +458,6 @@ class PlejdService extends EventEmitter { for (const path of Object.keys(objects)) { const interfaces = Object.keys(objects[path]); - console.log(interfaces); if (interfaces.indexOf(GATT_SERVICE_ID) > -1) { let chPaths = []; for (const c of characteristics) { @@ -492,14 +490,15 @@ class PlejdService extends EventEmitter { // After we've authenticated, we need to hook up the event listener // for changes to lastData. - this.characteristics.lastData.on('PropertiesChanged', this.onLastDataUpdated.bind(this)); + this.characteristics.lastDataProperties.on('PropertiesChanged', this.onLastDataUpdated.bind(this)); + this.characteristics.lastData.StartNotify(); } - //onLastDataUpdated(data, isNotification) { - onLastDataUpdated(iface, properties, invalidated) { + async onLastDataUpdated(iface, properties, invalidated) { if (iface !== GATT_CHRC_ID) { return; } + const changedKeys = Object.keys(properties); if (changedKeys.length === 0) { return; @@ -507,9 +506,12 @@ class PlejdService extends EventEmitter { console.log(properties); - const value = await properties.Get('Value'); + const value = await properties['Value']; + if (!value) { + return; + } + const data = value.value; - return; const decoded = this._encryptDecrypt(this.cryptoKey, this.plejdService.addr, data); let state = 0; From ce84a296ab99bed76b2661d8c3801339b2fda4d4 Mon Sep 17 00:00:00 2001 From: Marcus Westin Date: Mon, 20 Jan 2020 12:28:02 +0000 Subject: [PATCH 07/17] reconnect and version up --- plejd/ble.bluez.js | 5 ----- plejd/config.json | 2 +- plejd/main.js | 13 ++++++++++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/plejd/ble.bluez.js b/plejd/ble.bluez.js index 2e232a5..1133090 100644 --- a/plejd/ble.bluez.js +++ b/plejd/ble.bluez.js @@ -109,7 +109,6 @@ class PlejdService extends EventEmitter { } this.objectManager.on('InterfacesAdded', this.onInterfacesAdded.bind(this)); - this.objectManager.on('InterfacesRemoved', this.onInterfacesRemoved.bind(this)); this.adapter.SetDiscoveryFilter({ 'UUIDs': new dbus.Variant('as', [PLEJD_SERVICE]), @@ -200,10 +199,6 @@ class PlejdService extends EventEmitter { } } - async onInterfacesRemoved(path, interfaces) { - const [adapter, dev, service, characteristic] = path.split('/').slice(3); - } - updateSettings(settings) { if (settings.debug) { debug = 'console'; diff --git a/plejd/config.json b/plejd/config.json index 7da0a74..ffd5f95 100644 --- a/plejd/config.json +++ b/plejd/config.json @@ -1,6 +1,6 @@ { "name": "Plejd", - "version": "0.2.8", + "version": "0.3.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 aa6a678..1588174 100644 --- a/plejd/main.js +++ b/plejd/main.js @@ -3,7 +3,7 @@ const mqtt = require('./mqtt'); const fs = require('fs'); const PlejdService = require('./ble.bluez'); -const version = "0.2.8"; +const version = "0.3.0"; async function main() { console.log('starting Plejd add-on v. ' + version); @@ -15,7 +15,7 @@ async function main() { const client = new mqtt.MqttClient(config.mqttBroker, config.mqttUsername, config.mqttPassword); plejdApi.once('loggedIn', () => { - plejdApi.getCryptoKey();//(cryptoKey) => { + plejdApi.getCryptoKey(); plejdApi.on('ready', (cryptoKey) => { const devices = plejdApi.getDevices(); @@ -28,8 +28,15 @@ async function main() { // init the BLE interface const plejd = new PlejdService(cryptoKey, true); + plejd.on('connectFailed', () => { + setTimeout(() => { + console.log('plejd-ble: were unable to connect, will retry connection in 10 seconds.'); + plejd.init(); + }, 10000); + }); + plejd.init(); - + plejd.on('authenticated', () => { console.log('plejd: connected via bluetooth.'); }); From 58041b1d9917800412747fd0c4976a3b170c9da6 Mon Sep 17 00:00:00 2001 From: Marcus Westin Date: Mon, 20 Jan 2020 14:08:43 +0000 Subject: [PATCH 08/17] moved listening to plejd events --- plejd/ble.bluez.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/plejd/ble.bluez.js b/plejd/ble.bluez.js index 1133090..36b4e6a 100644 --- a/plejd/ble.bluez.js +++ b/plejd/ble.bluez.js @@ -160,8 +160,12 @@ class PlejdService extends EventEmitter { setTimeout(async () => { await this.onDeviceConnected(connectedDevice); - await this.adapter.StopDiscovery(); + + // After we've authenticated, we need to hook up the event listener + // for changes to lastData. + this.characteristics.lastDataProperties.on('PropertiesChanged', this.onLastDataUpdated.bind(this)); + this.characteristics.lastData.StartNotify(); }, 2000); } @@ -482,11 +486,6 @@ class PlejdService extends EventEmitter { onDeviceCharacteristicsComplete() { logger('onDeviceCharacteristicsComplete()'); this.authenticate(); - - // After we've authenticated, we need to hook up the event listener - // for changes to lastData. - this.characteristics.lastDataProperties.on('PropertiesChanged', this.onLastDataUpdated.bind(this)); - this.characteristics.lastData.StartNotify(); } async onLastDataUpdated(iface, properties, invalidated) { From c078683e710853c329261c3a46bfea61c4792583 Mon Sep 17 00:00:00 2001 From: Marcus Westin Date: Mon, 20 Jan 2020 14:10:30 +0000 Subject: [PATCH 09/17] moved again --- plejd/ble.bluez.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plejd/ble.bluez.js b/plejd/ble.bluez.js index 36b4e6a..2e09dcf 100644 --- a/plejd/ble.bluez.js +++ b/plejd/ble.bluez.js @@ -161,11 +161,6 @@ class PlejdService extends EventEmitter { setTimeout(async () => { await this.onDeviceConnected(connectedDevice); await this.adapter.StopDiscovery(); - - // After we've authenticated, we need to hook up the event listener - // for changes to lastData. - this.characteristics.lastDataProperties.on('PropertiesChanged', this.onLastDataUpdated.bind(this)); - this.characteristics.lastData.StartNotify(); }, 2000); } @@ -337,6 +332,11 @@ class PlejdService extends EventEmitter { // Start ping logger('onAuthenticated()'); this.startPing(); + + // After we've authenticated, we need to hook up the event listener + // for changes to lastData. + this.characteristics.lastDataProperties.on('PropertiesChanged', this.onLastDataUpdated.bind(this)); + this.characteristics.lastData.StartNotify(); } async startPing() { From 27e8326202d6af1f00e2a72853f6ff0c0510bbc8 Mon Sep 17 00:00:00 2001 From: Marcus Westin Date: Mon, 20 Jan 2020 14:11:58 +0000 Subject: [PATCH 10/17] clear previous ble devices --- plejd/ble.bluez.js | 1 + 1 file changed, 1 insertion(+) diff --git a/plejd/ble.bluez.js b/plejd/ble.bluez.js index 2e09dcf..0a48a9a 100644 --- a/plejd/ble.bluez.js +++ b/plejd/ble.bluez.js @@ -72,6 +72,7 @@ class PlejdService extends EventEmitter { this.objectManager.removeAllListeners(); } + this.bleDevices = []; clearInterval(this.pingRef); const bluez = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, '/'); From 3dd1ddf9bf39a75aec1a965e81206e38b2567896 Mon Sep 17 00:00:00 2001 From: Marcus Westin Date: Mon, 20 Jan 2020 14:17:06 +0000 Subject: [PATCH 11/17] small fixes --- plejd/ble.bluez.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plejd/ble.bluez.js b/plejd/ble.bluez.js index 0a48a9a..db1c1cb 100644 --- a/plejd/ble.bluez.js +++ b/plejd/ble.bluez.js @@ -3,9 +3,8 @@ const crypto = require('crypto'); const xor = require('buffer-xor'); const _ = require('lodash'); const EventEmitter = require('events'); -const sleep = require('sleep'); -let debug = 'console'; +let debug = ''; const getLogger = () => { const consoleLogger = msg => console.log('plejd-ble', msg); @@ -347,7 +346,7 @@ class PlejdService extends EventEmitter { this.pingRef = setInterval(async () => { logger('ping'); await this.ping(); - }, 3000); + }, 5000); await this.ping(); } From 6e524c71895513b3d3a86f309d33e1fcfb16aea3 Mon Sep 17 00:00:00 2001 From: Marcus Westin Date: Mon, 20 Jan 2020 14:20:40 +0000 Subject: [PATCH 12/17] removed logging --- plejd/ble.bluez.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/plejd/ble.bluez.js b/plejd/ble.bluez.js index db1c1cb..c9ea91f 100644 --- a/plejd/ble.bluez.js +++ b/plejd/ble.bluez.js @@ -498,8 +498,6 @@ class PlejdService extends EventEmitter { return; } - console.log(properties); - const value = await properties['Value']; if (!value) { return; From 37258d1200c5c6ec5b96a8f0995b3740e050c321 Mon Sep 17 00:00:00 2001 From: icanos Date: Mon, 20 Jan 2020 19:13:38 +0000 Subject: [PATCH 13/17] fixed broken dockerfile --- plejd/Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plejd/Dockerfile b/plejd/Dockerfile index 320ec88..1be39ba 100644 --- a/plejd/Dockerfile +++ b/plejd/Dockerfile @@ -13,6 +13,7 @@ COPY ./main.js /plejd/ COPY ./mqtt.js /plejd/ COPY ./package.json /plejd/ COPY ./ble.js /plejd/ +COPY ./ble.bluez.js /plejd/ ARG BUILD_ARCH @@ -33,6 +34,8 @@ RUN \ git \ nodejs=10.16.3-r0 \ npm=10.16.3-r0 \ + dbus-dev \ + glib-dev \ \ && npm config set unsafe-perm true From dc6faaf07e8d23bf55c9889cd3300eafe914a564 Mon Sep 17 00:00:00 2001 From: icanos Date: Mon, 20 Jan 2020 19:21:54 +0000 Subject: [PATCH 14/17] restored correct path --- plejd/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plejd/main.js b/plejd/main.js index 1588174..fe41c78 100644 --- a/plejd/main.js +++ b/plejd/main.js @@ -8,7 +8,7 @@ const version = "0.3.0"; async function main() { console.log('starting Plejd add-on v. ' + version); - const rawData = fs.readFileSync('plejd.json'); + const rawData = fs.readFileSync('/data/plejd.json'); const config = JSON.parse(rawData); const plejdApi = new api.PlejdApi(config.site, config.username, config.password); From 85a194b7947fdfa5843956fa6d707d4120aed76a Mon Sep 17 00:00:00 2001 From: icanos Date: Mon, 20 Jan 2020 21:32:46 +0000 Subject: [PATCH 15/17] optimization --- plejd/ble.bluez.js | 10 +++++----- plejd/main.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plejd/ble.bluez.js b/plejd/ble.bluez.js index c9ea91f..4ffaff3 100644 --- a/plejd/ble.bluez.js +++ b/plejd/ble.bluez.js @@ -294,12 +294,12 @@ class PlejdService extends EventEmitter { const self = this; try { - logger('sending challenge to device'); + //logger('sending challenge to device'); await this.characteristics.auth.WriteValue([0], {}); - logger('reading response from device'); + //logger('reading response from device'); const challenge = await this.characteristics.auth.ReadValue({}); - const response = this._createChallengeResponse(this.cryptoKey, challenge); - logger('responding to authenticate'); + const response = this._createChallengeResponse(this.cryptoKey, Buffer.from(challenge)); + //logger('responding to authenticate'); await this.characteristics.auth.WriteValue([...response], {}); this.emit('authenticated'); @@ -346,7 +346,7 @@ class PlejdService extends EventEmitter { this.pingRef = setInterval(async () => { logger('ping'); await this.ping(); - }, 5000); + }, 3000); await this.ping(); } diff --git a/plejd/main.js b/plejd/main.js index fe41c78..363b461 100644 --- a/plejd/main.js +++ b/plejd/main.js @@ -29,8 +29,8 @@ async function main() { // init the BLE interface const plejd = new PlejdService(cryptoKey, true); plejd.on('connectFailed', () => { + console.log('plejd-ble: were unable to connect, will retry connection in 10 seconds.'); setTimeout(() => { - console.log('plejd-ble: were unable to connect, will retry connection in 10 seconds.'); plejd.init(); }, 10000); }); From ff1952cd88e264fc26baf30270caa8e929c6fb80 Mon Sep 17 00:00:00 2001 From: Marcus Westin Date: Tue, 21 Jan 2020 14:24:02 +0000 Subject: [PATCH 16/17] added entities as devices and stability in ble --- plejd/api.js | 4 +++- plejd/ble.bluez.js | 39 ++++++++++++++++----------------------- plejd/mqtt.js | 12 ++++++++++-- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/plejd/api.js b/plejd/api.js index 0e41589..207d16a 100644 --- a/plejd/api.js +++ b/plejd/api.js @@ -144,7 +144,9 @@ class PlejdApi extends EventEmitter { name: device.title, type: type, typeName: name, - dimmable: dimmable + dimmable: dimmable, + version: plejdDevice.firmware.version, + serialNumber: plejdDevice.deviceId }; logger(JSON.stringify(newDevice)); diff --git a/plejd/ble.bluez.js b/plejd/ble.bluez.js index 4ffaff3..a25769b 100644 --- a/plejd/ble.bluez.js +++ b/plejd/ble.bluez.js @@ -72,6 +72,13 @@ class PlejdService extends EventEmitter { } this.bleDevices = []; + this.characteristics = { + data: null, + lastData: null, + lastDataProperties: null, + auth: null, + ping: null + }; clearInterval(this.pingRef); const bluez = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, '/'); @@ -301,12 +308,18 @@ class PlejdService extends EventEmitter { const response = this._createChallengeResponse(this.cryptoKey, Buffer.from(challenge)); //logger('responding to authenticate'); await this.characteristics.auth.WriteValue([...response], {}); - - this.emit('authenticated'); } catch (err) { console.log('plejd-ble: error: failed to authenticate: ' + err); } + + // auth done, start ping + await this.startPing(); + + // After we've authenticated, we need to hook up the event listener + // for changes to lastData. + this.characteristics.lastDataProperties.on('PropertiesChanged', this.onLastDataUpdated.bind(this)); + this.characteristics.lastData.StartNotify(); } async write(data, retry = true) { @@ -328,17 +341,6 @@ class PlejdService extends EventEmitter { } } - onAuthenticated() { - // Start ping - logger('onAuthenticated()'); - this.startPing(); - - // After we've authenticated, we need to hook up the event listener - // for changes to lastData. - this.characteristics.lastDataProperties.on('PropertiesChanged', this.onLastDataUpdated.bind(this)); - this.characteristics.lastData.StartNotify(); - } - async startPing() { console.log('startPing()'); clearInterval(this.pingRef); @@ -347,8 +349,6 @@ class PlejdService extends EventEmitter { logger('ping'); await this.ping(); }, 3000); - - await this.ping(); } onPingSuccess(nr) { @@ -480,12 +480,7 @@ class PlejdService extends EventEmitter { return; } - this.emit('deviceCharacteristicsComplete'); - } - - onDeviceCharacteristicsComplete() { - logger('onDeviceCharacteristicsComplete()'); - this.authenticate(); + await this.authenticate(); } async onLastDataUpdated(iface, properties, invalidated) { @@ -549,8 +544,6 @@ class PlejdService extends EventEmitter { console.log('wireEvents()'); const self = 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)); } diff --git a/plejd/mqtt.js b/plejd/mqtt.js index 39afbb9..d9607fa 100644 --- a/plejd/mqtt.js +++ b/plejd/mqtt.js @@ -35,11 +35,19 @@ const getSettingsTopic = () => `plejd/settings`; const getDiscoveryPayload = device => ({ schema: 'json', name: device.name, - unique_id: `light.plejd.${device.name.toLowerCase().replace(/ /g, '')}`, + unique_id: device.serialNumber, state_topic: getStateTopic(device), command_topic: getCommandTopic(device), optimistic: false, - brightness: `${device.dimmable}` + retain: true, + brightness: `${device.dimmable}`, + device: { + identifiers: device.serialNumber, + manufacturer: 'Plejd', + model: device.typeName, + name: device.name, + sw_version: device.version + } }); // #endregion From df4cdc1141fca706291ab1463e954ad191bb1d81 Mon Sep 17 00:00:00 2001 From: Marcus Westin Date: Fri, 24 Jan 2020 10:20:39 +0100 Subject: [PATCH 17/17] minor fixes --- plejd/ble.bluez.js | 10 ++++++++++ plejd/mqtt.js | 1 - plejd/package-lock.json | 8 -------- plejd/package.json | 1 - 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/plejd/ble.bluez.js b/plejd/ble.bluez.js index a25769b..3ddaa8f 100644 --- a/plejd/ble.bluez.js +++ b/plejd/ble.bluez.js @@ -323,6 +323,10 @@ class PlejdService extends EventEmitter { } async write(data, retry = true) { + if (!this.plejdService || !this.characteristics.data) { + return; + } + try { console.log('plejd-ble: sending ' + data.length + ' byte(s) of data to Plejd'); const encryptedData = this._encryptDecrypt(this.cryptoKey, this.plejdService.addr, data); @@ -480,6 +484,12 @@ class PlejdService extends EventEmitter { return; } + if (!this.characteristics.auth) { + console.log('plejd-ble: error: unable to connect to the Plejd mesh.'); + this.emit('connectFailed'); + return; + } + await this.authenticate(); } diff --git a/plejd/mqtt.js b/plejd/mqtt.js index d9607fa..698b536 100644 --- a/plejd/mqtt.js +++ b/plejd/mqtt.js @@ -39,7 +39,6 @@ const getDiscoveryPayload = device => ({ state_topic: getStateTopic(device), command_topic: getCommandTopic(device), optimistic: false, - retain: true, brightness: `${device.dimmable}`, device: { identifiers: device.serialNumber, diff --git a/plejd/package-lock.json b/plejd/package-lock.json index 4478971..90fb2b8 100644 --- a/plejd/package-lock.json +++ b/plejd/package-lock.json @@ -199,14 +199,6 @@ "type": "^1.0.1" } }, - "dbus": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/dbus/-/dbus-1.0.5.tgz", - "integrity": "sha512-itMup/0lcjnwV4DULJtI8q37Nky713KE2b1QM4+W/0zlzAsMNJZURBtyENtnJc6BlHouqbur++PXWeEKDuGulg==", - "requires": { - "nan": "^2.13.2" - } - }, "dbus-next": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/dbus-next/-/dbus-next-0.8.1.tgz", diff --git a/plejd/package.json b/plejd/package.json index 9e21fea..7d3646f 100644 --- a/plejd/package.json +++ b/plejd/package.json @@ -3,7 +3,6 @@ "@abandonware/bluetooth-hci-socket": "0.5.3-3", "axios": "^0.19.0", "buffer-xor": "^2.0.2", - "dbus": "^1.0.5", "dbus-next": "^0.8.1", "fs": "0.0.1-security", "jspack": "0.0.4",