diff --git a/plejd/Dockerfile b/plejd/Dockerfile index a19a91c..b8d2554 100644 --- a/plejd/Dockerfile +++ b/plejd/Dockerfile @@ -12,7 +12,6 @@ COPY ./config.json /plejd/ COPY ./main.js /plejd/ COPY ./mqtt.js /plejd/ COPY ./package.json /plejd/ -COPY ./ble.js /plejd/ COPY ./ble.bluez.js /plejd/ COPY ./scene.manager.js /plejd/ diff --git a/plejd/ble.js b/plejd/ble.js deleted file mode 100644 index 87d31c3..0000000 --- a/plejd/ble.js +++ /dev/null @@ -1,676 +0,0 @@ -const noble = require('@icanos/noble'); -const crypto = require('crypto'); -const xor = require('buffer-xor'); -const _ = require('lodash'); -const EventEmitter = require('events'); - -let debug = ''; - -const getLogger = () => { - const consoleLogger = msg => console.log('plejd', msg); - if (debug === 'console') { - return consoleLogger; - } - - // > /dev/null - return _.noop; -}; - -const logger = getLogger(); - -// UUIDs -const PLEJD_SERVICE = "31ba000160854726be45040c957391b5" -const DATA_UUID = "31ba000460854726be45040c957391b5" -const LAST_DATA_UUID = "31ba000560854726be45040c957391b5" -const AUTH_UUID = "31ba000960854726be45040c957391b5" -const PING_UUID = "31ba000a60854726be45040c957391b5" - -const STATE_IDLE = 'idle'; -const STATE_SCANNING = 'scanning'; -const STATE_CONNECTING = 'connecting'; -const STATE_CONNECTED = 'connected'; -const STATE_AUTHENTICATED = 'authenticated'; -const STATE_DISCONNECTED = 'disconnected'; -const STATE_UNINITIALIZED = 'uninitialized'; -const STATE_INITIALIZED = 'initialized'; - -const BLE_CMD_DIM_CHANGE = '00c8'; -const BLE_CMD_DIM2_CHANGE = '0098'; -const BLE_CMD_STATE_CHANGE = '0097'; -const BLE_CMD_SCENE_TRIG = '0021'; - -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.characteristicState = STATE_UNINITIALIZED; - this.characteristics = { - data: null, - lastData: null, - auth: null, - ping: null - }; - - logger('wiring events and waiting for BLE interface to power up.'); - this.wireEvents(); - } - - 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) { - let sortedDevices = Object.values(this.devices).sort((a, b) => b.rssi - a.rssi); - this.device = sortedDevices[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; - this.characteristicState = STATE_UNINITIALIZED; - } - - 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('stopping ping and reconnecting.'); - 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('scanStop', this.onDeviceScanComplete.bind(self)); - noble.on('discover', this.onDeviceDiscovered.bind(self)); - noble.on('disconnect', this.onDeviceDisconnected.bind(self)); - - this.on('scanComplete', this.onDeviceScanComplete.bind(this)); - this.on('deviceCharacteristicsComplete', this.onDeviceCharacteristicsComplete.bind(self)); - this.on('authenticated', this.onAuthenticated.bind(self)); - this.on('pingFailed', this.onPingFailed.bind(self)); - this.on('pingSuccess', this.onPingSuccess.bind(self)); - } - - subscribeCharacteristics() { - if (this.characteristics.lastData) { - this.characteristics.lastData.subscribe((err) => { - if (err) { - console.log('error: could not subscribe to event.'); - } - }); - this.characteristics.lastData.on('data', this.onLastDataUpdated.bind(this)); - } - } - - unsubscribeCharacteristics() { - if (this.characteristics.lastData) { - 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/plejd.js b/plejd/plejd.js deleted file mode 100644 index 17d90c6..0000000 --- a/plejd/plejd.js +++ /dev/null @@ -1,505 +0,0 @@ -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 = ''; - -const getLogger = () => { - const consoleLogger = msg => console.log('plejd', msg); - if (debug === 'console') { - return consoleLogger; - } - - // > /dev/null - return _.noop; -}; - -const logger = getLogger(); - -// UUIDs -const PLEJD_SERVICE = "31ba000160854726be45040c957391b5" -const DATA_UUID = "31ba000460854726be45040c957391b5" -const LAST_DATA_UUID = "31ba000560854726be45040c957391b5" -const AUTH_UUID = "31ba000960854726be45040c957391b5" -const PING_UUID = "31ba000a60854726be45040c957391b5" - -class Controller extends EventEmitter { - constructor(cryptoKey, keepAlive = false) { - super(); - - this.cryptoKey = Buffer.from(cryptoKey.replace(/-/g, ''), 'hex'); - this.peripheral = null; - this.peripheral_address = null; - - this.isScanning = false; - this.isConnecting = false; - this.isConnected = false; - this.keepAlive = keepAlive; - this.writeQueue = []; - this.peripherals = []; - - // Holds a reference to the connected peripheral from the peripheral list. - // In case the peripheral we're connecting to, disconnects us, we can then reinitiate the connection - // by increasing the connectedIndex and by that, connect to the next in line. - this.connectedIndex = 0; - } - - async init() { - const self = this; - - noble.on('stateChange', async (state) => { - logger('ble state changed: ' + state); - - if (state === 'poweredOn') { - await this.scan(); - } - }); - - noble.on('discover', (peripheral) => { - logger('found ' + peripheral.advertisement.localName + ' with addr ' + peripheral.address); - if (peripheral.advertisement.localName === 'P mesh') { - self.peripherals.push(peripheral); - } - }); - - noble.on('disconnect', async () => { - if (self.peripherals.length) { - logger('peripherals already scanned.'); - this.connectedIndex = 0; - await self.connect(); - } - }); - } - - async reinit() { - console.log('reinitializing the Plejd add-on.'); - this.once('scanComplete', async (peripherals) => { - console.log('found Plejd devices, reconnecting'); - await this.connect(); - }); - - await this.scan(); - } - - async scan() { - const self = this; - this.isScanning = true; - noble.startScanning([PLEJD_SERVICE]); - - setTimeout(() => { - noble.stopScanning(); - this.isScanning = false; - - self.peripherals.sort((a, b) => a.rssi > b.rssi); - this.emit('scanComplete', self.peripherals); - - }, 5000); - } - - async connect() { - const self = this; - - if (this.isScanning) { - logger('already scanning, waiting.'); - return Promise.resolve(false); - } - - // if (!this.peripherals.length) { - // await this.scan(); - // } - - this.isConnecting = true; - - return await this._internalConnect(this.connectedIndex); - } - - async _internalConnect(idx) { - const self = this; - - if (idx >= this.peripherals.length) { - logger('reached end of list.'); - return Promise.resolve(false); - } - - logger('connecting to Plejd device'); - try { - this.peripherals[idx].connect(async (err) => { - if (err) { - console.log('error: failed to connect to Plejd device: ' + err); - return await self._internalConnect(idx + 1); - } - - self.peripheral = self.peripherals[idx]; - console.log('connected to Plejd device with addr ' + self.peripheral.address + ' with rssi ' + self.peripheral.rssi); - - self.peripheral_address = self._reverseBuffer(Buffer.from(String(self.peripheral.address).replace(/\-/g, '').replace(/\:/g, ''), 'hex')); - - let successfullyReadCharacteristics = false; - logger('discovering services and characteristics'); - - setTimeout(async () => { - if (!successfullyReadCharacteristics) { - logger('error: timed out when reading characteristics. moving on to next device.'); - return await self._internalConnect(idx + 1); - } - }, 10000); - - await self.peripheral.discoverSomeServicesAndCharacteristics([PLEJD_SERVICE], [], async (err, services, characteristics) => { - //await self.peripheral.discoverAllServicesAndCharacteristics(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.dataCharacteristic = ch; - } - else if (LAST_DATA_UUID == ch.uuid) { - logger('found LAST_DATA characteristic.'); - self.lastDataCharacteristic = ch; - } - else if (AUTH_UUID == ch.uuid) { - logger('found AUTH characteristic.'); - self.authCharacteristic = ch; - } - else if (PING_UUID == ch.uuid) { - logger('found PING characteristic.'); - self.pingCharacteristic = ch; - } - }); - - if (this.dataCharacteristic - && this.lastDataCharacteristic - && this.authCharacteristic - && this.pingCharacteristic) { - - successfullyReadCharacteristics = true; - - this.once('authenticated', () => { - logger('Plejd is connected and authenticated.'); - this.connectedIndex = idx; - - if (self.keepAlive) { - self.startPing(); - } - - self.subscribe(); - - self.emit('connected'); - }); - - try { - await this.authenticate(); - } - catch (error) { - this.isConnecting = false; - console.log('error: failed to authenticate: ' + error); - return Promise.resolve(false); - } - - this.isConnected = true; - this.isConnecting = false; - - // make sure to write any queued up messages to the Plejd devices - if (this.writeQueue && this.writeQueue.length > 0) { - this.flush(); - } - } - - return Promise.resolve(true); - }); - }); - } - catch (error) { - this.isConnecting = false; - - console.log('error: failed to connect to Plejd device: ' + error); - } - - return Promise.resolve(true); - } - - subscribe() { - const self = this; - - self.lastDataCharacteristic.subscribe((err) => { - if (err) { - console.log('error: couldnt subscribe to notification characteristic.'); - return; - } - - // subscribe to last data event - self.lastDataCharacteristic.on('data', (data, isNotification) => { - const decoded = self._encryptDecrypt(self.cryptoKey, self.peripheral_address, data); - - let state = 0; - let dim = 0; - let device = parseInt(decoded[0], 10); - - if (decoded.toString('hex', 3, 5) === '00c8' || decoded.toString('hex', 3, 5) === '0098') { - state = parseInt(decoded.toString('hex', 5, 6), 10); - dim = parseInt(decoded.toString('hex', 6, 8), 16) >> 8; - - logger('d: ' + device + ' got state+dim update: ' + state + ' - ' + dim); - this.emit('dimChanged', device, state, dim); - } - else if (decoded.toString('hex', 3, 5) === '0097') { - state = parseInt(decoded.toString('hex', 5, 6), 10); - logger('d: ' + device + ' got state update: ' + state); - this.emit('stateChanged', device, state); - } - }); - }); - } - - async disconnect() { - logger('disconnecting from Plejd'); - - if (this.isConnected) { - clearInterval(this.pingRef); - - if (this.peripheral) { - try { - // disconnect - await this.peripheral.disconnect(); - - // we need to reset the ble adapter too - noble._bindings._hci.reset(); - - // wait 200 ms for reset command to take effect :) - sleep.msleep(200); - - // now we're ready to connect again - } - catch (error) { - console.log('error: unable to disconnect from Plejd: ' + error); - return Promise.resolve(false); - } - - this.isConnected = false; - logger('disconnected from Plejd'); - - return Promise.resolve(true); - } - } - else { - clearInterval(this.pingRef); - this.isConnected = false; - logger('disconnected from Plejd'); - - return Promise.resolve(true); - } - } - - async turnOn(id, brightness) { - // if (this.peripheral.state !== 'connected') { - // console.log('warning: not connected, will connect. might take a few seconds.'); - // await this.reinit(); - // } - - 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); - } - - async turnOff(id) { - // if (this.peripheral.state !== 'connected') { - // console.log('warning: not connected, will connect. might take a few seconds.'); - // await this.reinit(); - // } - - logger('turning off ' + id); - - var payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009700', 'hex'); - this.write(payload); - } - - startPing() { - const self = this; - - clearInterval(this.pingRef); - logger('starting ping'); - this.pingRef = setInterval(async () => { - logger('ping'); - if (self.peripheral.state == 'connected') { - await self.plejdPing(async (pingOk) => { - - if (!pingOk) { - console.log('error: ping failed'); - await self.disconnect(); - // await self.reinit(); - } - else { - logger('pong'); - } - }); - } - else { - await self.disconnect(); - // await self.reinit(); - } - }, 3000); - } - - async plejdPing(callback) { - var ping = crypto.randomBytes(1); - - try { - // make sure we're connected, otherwise, return false and reconnect - // if (this.peripheral.state != 'connected') { - // callback(false); - // return; - // } - - this.pingCharacteristic.write(ping, false, (err) => { - if (err) { - console.log('error: unable to send ping: ' + err); - callback(false); - } - - this.pingCharacteristic.read((err, data) => { - if (err) { - console.log('error: unable to read ping: ' + err); - callback(false); - } - - if (((ping[0] + 1) & 0xff) !== data[0]) { - callback(false); - } - else { - callback(true); - } - }); - }); - } - catch (error) { - console.log('error: writing to plejd: ' + error); - callback(false); - } - } - - async authenticate() { - const self = this; - - logger('authenticating connection'); - this.authCharacteristic.write(Buffer.from([0]), false, (err) => { - if (err) { - console.log('error: failed to authenticate: ' + err); - return; - } - - this.authCharacteristic.read(async (err2, data) => { - if (err2) { - console.log('error: challenge request failed: ' + err2); - return; - } - - var resp = self._challengeResponse(self.cryptoKey, data); - - this.authCharacteristic.write(resp, false, (err3) => { - if (err3) { - console.log('error: challenge failed: ' + err2); - return; - } - - this.emit('authenticated'); - }); - }); - }); - } - - async write(data) { - const self = this; - - try { - if (this.peripheral.state !== 'connected') { - logger('adding message to queue.'); - this.writeQueue.push(data); - return Promise.resolve(true); - } - - if (!this.keepAlive) { - logger('not connected to Plejd. reconnecting.'); - await this.connect(); - } - - logger('writing ' + data + ' to ' + this.peripheral.address); - this.dataCharacteristic.write(this._encryptDecrypt(this.cryptoKey, this.peripheral_address, data), false); - this.flush(); - - if (!this.keepAlive) { - clearTimeout(this.disconnectIntervalRef); - this.disconnectIntervalRef = setTimeout(async () => { - await self.disconnect(); - }, 5000); - } - } - catch (error) { - console.log('error: writing to plejd: ' + error); - await self.disconnect(); - // await self.connect(); - } - } - - async flush() { - let writeData; - while ((writeData = this.writeQueue.shift()) !== undefined) { - this.dataCharacteristic.write(this._encryptDecrypt(this.cryptoKey, this.peripheral_address, writeData), false); - } - } - - _challengeResponse(key, challenge) { - const intermediate = crypto.createHash('sha256').update(xor(key, challenge)).digest(); - const part1 = intermediate.subarray(0, 16); - const part2 = intermediate.subarray(16); - - const resp = xor(part1, part2); - - return resp; - } - - _encryptDecrypt(key, addr, data) { - var buf = Buffer.concat([addr, addr, addr.subarray(0, 4)]); - - var cipher = crypto.createCipheriv("aes-128-ecb", key, ''); - cipher.setAutoPadding(false); - - var ct = cipher.update(buf).toString('hex'); - ct += cipher.final().toString('hex'); - ct = Buffer.from(ct, 'hex'); - - var output = ""; - for (var i = 0, length = data.length; i < length; i++) { - output += String.fromCharCode(data[i] ^ ct[i % 16]); - } - - return Buffer.from(output, 'ascii'); - } - - _reverseBuffer(src) { - var buffer = Buffer.allocUnsafe(src.length) - - for (var i = 0, j = src.length - 1; i <= j; ++i, --j) { - buffer[i] = src[j] - buffer[j] = src[i] - } - - return buffer - } -} - -module.exports = { Controller }; \ No newline at end of file