hassio-plejd/plejd/ble.bluez.js

686 lines
21 KiB
JavaScript
Raw Normal View History

2020-01-18 15:50:46 +00:00
const dbus = require('dbus-next');
2020-01-17 14:50:58 +00:00
const crypto = require('crypto');
const xor = require('buffer-xor');
const _ = require('lodash');
const EventEmitter = require('events');
2020-01-20 14:17:06 +00:00
let debug = '';
2020-01-17 14:50:58 +00:00
const getLogger = () => {
const consoleLogger = (...msg) => console.log('plejd-ble', ...msg);
2020-01-17 14:50:58 +00:00
if (debug === 'console') {
return consoleLogger;
}
// > /dev/null
return _.noop;
};
const logger = getLogger();
// UUIDs
2020-01-19 20:08:48 +00:00
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';
2020-01-17 14:50:58 +00:00
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';
2020-01-18 15:50:46 +00:00
const DBUS_OM_INTERFACE = 'org.freedesktop.DBus.ObjectManager';
const DBUS_PROP_INTERFACE = 'org.freedesktop.DBus.Properties';
2020-01-17 14:50:58 +00:00
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';
const MAX_TRANSITION_STEPS_PER_SECOND = 5; // Could be made a setting
const MAX_WRITEQUEUE_LENGTH_TARGET = 0; // Could be made a setting. 0 => queue length = numDevices => 1 command pending per device max
2020-01-17 14:50:58 +00:00
class PlejdService extends EventEmitter {
constructor(cryptoKey, devices, sceneManager, connectionTimeout, writeQueueWaitTime, keepAlive = false) {
2020-01-17 14:50:58 +00:00
super();
this.cryptoKey = Buffer.from(cryptoKey.replace(/-/g, ''), 'hex');
this.sceneManager = sceneManager;
this.connectedDevice = null;
2020-01-19 20:08:48 +00:00
this.plejdService = null;
this.bleDevices = [];
this.bleDeviceTransitionTimers = {};
2020-01-17 14:50:58 +00:00
this.plejdDevices = {};
this.devices = devices;
2020-01-17 14:50:58 +00:00
this.connectEventHooked = false;
2020-01-27 20:43:52 +00:00
this.connectionTimeout = connectionTimeout;
this.writeQueueWaitTime = writeQueueWaitTime;
this.writeQueue = [];
this.writeQueueRef = null;
this.maxQueueLengthTarget = MAX_WRITEQUEUE_LENGTH_TARGET || this.devices.length || 5;
logger('Max global transition queue length target', this.maxQueueLengthTarget)
2020-01-17 14:50:58 +00:00
// Holds a reference to all characteristics
this.characteristics = {
data: null,
lastData: null,
2020-01-19 20:08:48 +00:00
lastDataProperties: null,
2020-01-17 14:50:58 +00:00
auth: null,
ping: null
};
2020-01-18 15:50:46 +00:00
this.bus = dbus.systemBus();
2020-01-19 20:08:48 +00:00
this.adapter = null;
2020-01-17 14:50:58 +00:00
logger('wiring events and waiting for BLE interface to power up.');
this.wireEvents();
}
async init() {
if (this.objectManager) {
this.objectManager.removeAllListeners();
}
this.connectedDevice = null;
this.characteristics = {
data: null,
lastData: null,
lastDataProperties: null,
auth: null,
ping: null
};
2020-01-27 20:43:52 +00:00
2020-01-20 10:58:03 +00:00
clearInterval(this.pingRef);
clearInterval(this.writeQueueRef);
2020-01-27 20:43:52 +00:00
console.log('init()');
2020-01-20 10:58:03 +00:00
2020-01-18 15:50:46 +00:00
const bluez = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, '/');
this.objectManager = await bluez.getInterface(DBUS_OM_INTERFACE);
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
// 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);
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
if (result) {
this.adapter = result[1];
}
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
if (!this.adapter) {
console.log('plejd-ble: error: unable to find a bluetooth adapter that is compatible.');
return;
2020-01-17 14:50:58 +00:00
}
2020-01-19 20:08:48 +00:00
for (let path of Object.keys(managedObjects)) {
const interfaces = Object.keys(managedObjects[path]);
2020-01-19 20:08:48 +00:00
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);
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
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);
}
2020-01-17 14:50:58 +00:00
}
2020-01-19 20:08:48 +00:00
this.objectManager.on('InterfacesAdded', this.onInterfacesAdded.bind(this));
this.adapter.SetDiscoveryFilter({
'UUIDs': new dbus.Variant('as', [PLEJD_SERVICE]),
'Transport': new dbus.Variant('s', 'le')
});
2020-01-27 20:43:52 +00:00
try {
await this.adapter.StartDiscovery();
2020-06-12 11:15:00 +02:00
} catch (err) {
2020-01-27 20:43:52 +00:00
console.log('plejd-ble: error: failed to start discovery. Make sure no other add-on is currently scanning.');
return;
}
2020-01-19 20:08:48 +00:00
setTimeout(async () => {
await this._internalInit();
2020-01-27 20:43:52 +00:00
}, this.connectionTimeout * 1000);
2020-01-19 20:08:48 +00:00
}
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
async _internalInit() {
logger('got ', this.bleDevices.length, ' device(s).');
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
for (const plejd of this.bleDevices) {
logger('inspecting ', plejd['path']);
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
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);
2020-01-19 20:08:48 +00:00
plejd['rssi'] = (await properties.Get(BLUEZ_DEVICE_ID, 'RSSI')).value;
plejd['instance'] = device;
2020-01-17 14:50:58 +00:00
const segments = plejd['path'].split('/');
let fixedPlejdPath = segments[segments.length - 1].replace('dev_', '');
fixedPlejdPath = fixedPlejdPath.replace(/_/g, '');
plejd['device'] = this.devices.find(x => x.serialNumber === fixedPlejdPath);
logger('discovered ', plejd['path'] + ' with rssi ' + plejd['rssi']);
2020-06-12 11:15:00 +02:00
} catch (err) {
2020-01-19 20:08:48 +00:00
console.log('plejd-ble: failed inspecting ' + plejd['path'] + ' error: ' + err);
2020-01-17 14:50:58 +00:00
}
2020-01-19 20:08:48 +00:00
}
const sortedDevices = this.bleDevices.sort((a, b) => b['rssi'] - a['rssi']);
let connectedDevice = null;
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
for (const plejd of sortedDevices) {
try {
2020-01-27 20:43:52 +00:00
if (plejd['instance']) {
console.log('plejd-ble: connecting to ' + plejd['path']);
await plejd['instance'].Connect();
connectedDevice = plejd;
break
}
2020-06-12 11:15:00 +02:00
} catch (err) {
2020-01-27 20:43:52 +00:00
console.log('plejd-ble: warning: unable to connect, will retry. ' + err);
2020-01-19 20:08:48 +00:00
}
2020-01-17 14:50:58 +00:00
}
2020-01-19 20:08:48 +00:00
setTimeout(async () => {
await this.onDeviceConnected(connectedDevice);
await this.adapter.StopDiscovery();
2020-01-27 20:43:52 +00:00
}, this.connectionTimeout * 1000);
2020-01-19 20:08:48 +00:00
}
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
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) {
logger('found ble interface \'', iface, '\' at ', path);
2020-01-19 20:08:48 +00:00
try {
const adapterObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path);
return [path, adapterObject.getInterface(iface), adapterObject];
2020-06-12 11:15:00 +02:00
} catch (err) {
2020-01-19 20:08:48 +00:00
console.log('plejd-ble: error: failed to get interface \'' + iface + '\': ' + err);
}
2020-01-17 14:50:58 +00:00
}
2020-01-19 20:08:48 +00:00
}
return null;
}
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
async onInterfacesAdded(path, interfaces) {
// const [adapter, dev, service, characteristic] = path.split('/').slice(3);
2020-01-19 20:08:48 +00:00
const interfaceKeys = Object.keys(interfaces);
if (interfaceKeys.indexOf(BLUEZ_DEVICE_ID) > -1) {
if (interfaces[BLUEZ_DEVICE_ID]['UUIDs'].value.indexOf(PLEJD_SERVICE) > -1) {
logger('found Plejd service on ', path);
2020-06-12 11:15:00 +02:00
this.bleDevices.push({
'path': path
});
2020-01-19 20:08:48 +00:00
} else {
console.log('uh oh, no Plejd device.');
2020-01-17 14:50:58 +00:00
}
}
}
updateSettings(settings) {
if (settings.debug) {
debug = 'console';
2020-06-12 11:15:00 +02:00
} else {
2020-01-17 14:50:58 +00:00
debug = '';
}
}
turnOn(id, command) {
console.log('Plejd got turn on command for ', id, ', brightness ', command.brightness, ', transition ', command.transition);
this._transitionTo(id, command.brightness, command.transition);
}
2020-01-17 14:50:58 +00:00
turnOff(id, command) {
console.log('Plejd got turn off command for ', id, ', transition ', command.transition);
this._transitionTo(id, 0, command.transition);
}
2020-01-17 14:50:58 +00:00
_clearDeviceTransitionTimer(id) {
if (this.bleDeviceTransitionTimers[id]) {
clearInterval(this.bleDeviceTransitionTimers[id]);
2020-01-17 14:50:58 +00:00
}
}
_transitionTo(id, targetBrightness, transition) {
const initialBrightness = this.plejdDevices[id] ? this.plejdDevices[id].dim : null;
this._clearDeviceTransitionTimer(id);
2020-01-17 14:50:58 +00:00
const isDimmable = this.devices.find(d => d.id === id).dimmable;
2020-01-17 14:50:58 +00:00
if (transition > 1 && isDimmable && (initialBrightness || initialBrightness === 0) && (targetBrightness || targetBrightness === 0) && targetBrightness !== initialBrightness) {
// Transition time set, known initial and target brightness
// Calculate transition interval time based on delta brightness and max steps per second
// During transition, measure actual transition interval time and adjust stepping continously
// If transition <= 1 second, Plejd will do a better job than we can in transitioning so transitioning will be skipped
const deltaBrightness = targetBrightness - initialBrightness;
const transitionSteps = Math.min(Math.abs(deltaBrightness), MAX_TRANSITION_STEPS_PER_SECOND * transition);
const transitionInterval = transition * 1000 / transitionSteps;
logger('transitioning from', initialBrightness, 'to', targetBrightness, 'in', transition, 'seconds.');
logger('delta brightness', deltaBrightness, ', steps ', transitionSteps, ', interval', transitionInterval, 'ms');
const dtStart = new Date();
let nSteps = 0;
let nSkippedSteps = 0;
this.bleDeviceTransitionTimers[id] = setInterval(() => {
let tElapsedMs = (new Date().getTime() - dtStart.getTime());
let tElapsed = tElapsedMs / 1000;
if (tElapsed > transition || tElapsed < 0) {
tElapsed = transition;
2020-01-17 14:50:58 +00:00
}
let newBrightness = parseInt(initialBrightness + deltaBrightness * tElapsed / transition);
2020-01-17 14:50:58 +00:00
if (tElapsed === transition) {
nSteps++;
this._clearDeviceTransitionTimer(id);
newBrightness = targetBrightness;
logger('Completing transition from', initialBrightness, 'to', targetBrightness, 'in ', tElapsedMs, 'ms. Done steps', nSteps, ', skipped ' + nSkippedSteps + '. Average interval', tElapsedMs/(nSteps||1), 'ms.');
this._setBrightness(id, newBrightness);
}
2021-01-01 20:48:53 +01:00
else if (this.writeQueue.length <= this.maxQueueLengthTarget) {
nSteps++;
this._setBrightness(id, newBrightness);
}
else {
nSkippedSteps++;
2021-01-01 20:48:53 +01:00
logger('Skipping transition step due to write queue full as configured. Queue length', this.writeQueue.length, ', max', this.maxQueueLengthTarget);
}
}, transitionInterval);
}
else {
if (transition && isDimmable) {
logger('Could not transition light change. Either initial value is unknown or change is too small. Requested from', initialBrightness, 'to', targetBrightness)
}
this._setBrightness(id, targetBrightness);
}
}
_setBrightness(id, brightness) {
if (!brightness && brightness !== 0) {
logger('no brightness specified, setting ', id, ' to previous known.');
var payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009701', 'hex');
this.writeQueue.unshift(payload);
}
else {
if (brightness <= 0) {
this._turnOff(id);
}
else {
if (brightness > 255) {
brightness = 255;
}
logger('Setting ', id, 'brightness to ' + brightness);
brightness = brightness << 8 | brightness;
var payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009801' + (brightness).toString(16).padStart(4, '0'), 'hex');
}
this.writeQueue.unshift(payload);
2020-01-17 14:50:58 +00:00
}
}
_turnOff(id) {
logger('Turning off ', id);
2020-01-17 14:50:58 +00:00
var payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009700', 'hex');
this.writeQueue.unshift(payload);
}
triggerScene(sceneIndex) {
console.log('triggering scene with ID', sceneIndex);
this.sceneManager.executeScene(sceneIndex, this);
2020-01-17 14:50:58 +00:00
}
2020-01-19 20:08:48 +00:00
async authenticate() {
2020-01-17 14:50:58 +00:00
console.log('authenticate()');
const self = this;
2020-01-19 20:08:48 +00:00
try {
2020-01-20 21:32:46 +00:00
//logger('sending challenge to device');
2020-01-19 20:08:48 +00:00
await this.characteristics.auth.WriteValue([0], {});
2020-01-20 21:32:46 +00:00
//logger('reading response from device');
2020-01-19 20:08:48 +00:00
const challenge = await this.characteristics.auth.ReadValue({});
2020-01-20 21:32:46 +00:00
const response = this._createChallengeResponse(this.cryptoKey, Buffer.from(challenge));
//logger('responding to authenticate');
2020-01-19 20:08:48 +00:00
await this.characteristics.auth.WriteValue([...response], {});
2020-06-12 11:15:00 +02:00
} catch (err) {
2020-01-19 20:08:48 +00:00
console.log('plejd-ble: error: failed to authenticate: ' + err);
}
// auth done, start ping
await this.startPing();
await this.startWriteQueue();
// 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();
2020-01-17 14:50:58 +00:00
}
2020-01-19 20:08:48 +00:00
async write(data, retry = true) {
2020-01-24 10:20:39 +01:00
if (!this.plejdService || !this.characteristics.data) {
return;
}
2020-01-19 20:08:48 +00:00
try {
logger('sending ', data.length, ' byte(s) of data to Plejd', data);
2020-01-19 20:08:48 +00:00
const encryptedData = this._encryptDecrypt(this.cryptoKey, this.plejdService.addr, data);
await this.characteristics.data.WriteValue([...encryptedData], {});
2020-06-12 11:15:00 +02:00
} catch (err) {
2020-03-03 15:59:10 +01:00
if (err.message === 'In Progress') {
setTimeout(() => this.write(data, retry), 1000);
return;
}
2020-01-19 20:08:48 +00:00
console.log('plejd-ble: write failed ' + err);
2020-01-20 10:58:03 +00:00
setTimeout(async () => {
await this.init();
2020-01-19 20:08:48 +00:00
2020-01-20 10:58:03 +00:00
if (retry) {
logger('reconnected and retrying to write');
await this.write(data, false);
}
2020-01-27 20:43:52 +00:00
}, this.connectionTimeout * 1000);
2020-01-19 20:08:48 +00:00
}
2020-01-17 14:50:58 +00:00
}
2020-01-19 20:08:48 +00:00
async startPing() {
2020-01-17 14:50:58 +00:00
console.log('startPing()');
clearInterval(this.pingRef);
this.pingRef = setInterval(async () => {
2020-01-19 20:08:48 +00:00
logger('ping');
await this.ping();
2020-01-20 21:32:46 +00:00
}, 3000);
2020-01-17 14:50:58 +00:00
}
onPingSuccess(nr) {
logger('pong: ' + nr);
}
2020-01-19 20:08:48 +00:00
async onPingFailed(error) {
2020-01-17 14:50:58 +00:00
logger('onPingFailed(' + error + ')');
2020-01-19 20:08:48 +00:00
console.log('plejd-ble: ping failed, reconnecting.');
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
clearInterval(this.pingRef);
await this.init();
2020-01-17 14:50:58 +00:00
}
2020-01-19 20:08:48 +00:00
async ping() {
2020-01-17 14:50:58 +00:00
logger('ping()');
2020-01-19 20:08:48 +00:00
var ping = crypto.randomBytes(1);
let pong = null;
try {
await this.characteristics.ping.WriteValue([...ping], {});
pong = await this.characteristics.ping.ReadValue({});
2020-06-12 11:15:00 +02:00
} catch (err) {
2020-01-19 20:08:48 +00:00
console.log('error: writing to plejd: ' + err);
this.emit('pingFailed', 'write error');
2020-01-17 14:50:58 +00:00
return;
}
2020-01-19 20:08:48 +00:00
if (((ping[0] + 1) & 0xff) !== pong[0]) {
console.log('error: plejd ping failed');
this.emit('pingFailed', 'plejd ping failed ' + ping[0] + ' - ' + pong[0]);
return;
}
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
this.emit('pingSuccess', pong[0]);
2020-01-17 14:50:58 +00:00
}
async startWriteQueue() {
console.log('startWriteQueue()');
clearInterval(this.writeQueueRef);
this.writeQueueRef = setTimeout(() => this.runWriteQueue(), this.writeQueueWaitTime);
2020-03-03 15:59:10 +01:00
}
async runWriteQueue() {
while (this.writeQueue.length > 0) {
const data = this.writeQueue.pop();
await this.write(data, true);
}
this.writeQueueRef = setTimeout(() => this.runWriteQueue(), this.writeQueueWaitTime);
}
2020-01-19 20:08:48 +00:00
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);
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
const uuid = (await properties.Get(GATT_SERVICE_ID, 'UUID')).value;
if (uuid !== PLEJD_SERVICE) {
console.log('plejd-ble: not a Plejd device.');
return null;
}
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
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])
2020-06-12 11:15:00 +02:00
.replace(/\-/g, '')
.replace(/\_/g, '')
.replace(/\:/g, ''), 'hex'
2020-01-19 20:08:48 +00:00
)
);
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;
2020-06-12 11:15:00 +02:00
} else if (chUuid === LAST_DATA_UUID) {
2020-01-19 20:08:48 +00:00
logger('found LAST_DATA characteristic.');
this.characteristics.lastData = ch;
this.characteristics.lastDataProperties = prop;
2020-06-12 11:15:00 +02:00
} else if (chUuid === AUTH_UUID) {
2020-01-19 20:08:48 +00:00
logger('found AUTH characteristic.');
this.characteristics.auth = ch;
2020-06-12 11:15:00 +02:00
} else if (chUuid === PING_UUID) {
2020-01-19 20:08:48 +00:00
logger('found PING characteristic.');
this.characteristics.ping = ch;
}
}
return {
2020-01-20 10:58:03 +00:00
addr: addr
2020-01-19 20:08:48 +00:00
};
2020-01-17 14:50:58 +00:00
}
2020-01-19 20:08:48 +00:00
async onDeviceConnected(device) {
console.log('onDeviceConnected()');
const objects = await this.objectManager.GetManagedObjects();
2020-01-24 17:30:17 +00:00
const paths = Object.keys(objects);
2020-01-19 20:08:48 +00:00
let characteristics = [];
2020-01-24 17:30:17 +00:00
for (const path of paths) {
2020-01-19 20:08:48 +00:00
const interfaces = Object.keys(objects[path]);
if (interfaces.indexOf(GATT_CHRC_ID) > -1) {
characteristics.push(path);
}
2020-01-17 14:50:58 +00:00
}
2020-01-24 17:30:17 +00:00
for (const path of paths) {
2020-01-19 20:08:48 +00:00
const interfaces = Object.keys(objects[path]);
if (interfaces.indexOf(GATT_SERVICE_ID) > -1) {
let chPaths = [];
for (const c of characteristics) {
2020-01-24 17:30:17 +00:00
if (c.startsWith(path + '/')) {
2020-01-19 20:08:48 +00:00
chPaths.push(c);
}
}
console.log('trying ' + chPaths.length + ' characteristics');
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
this.plejdService = await this._processPlejdService(path, chPaths);
if (this.plejdService) {
break;
}
}
}
if (!this.plejdService) {
2020-01-27 20:43:52 +00:00
console.log('plejd-ble: warning: wasn\'t able to connect to Plejd, will retry.');
this.emit('connectFailed');
2020-01-17 14:50:58 +00:00
return;
}
2020-01-24 10:20:39 +01:00
if (!this.characteristics.auth) {
2020-01-24 17:30:17 +00:00
console.log('plejd-ble: error: unable to enumerate characteristics.');
2020-01-24 10:20:39 +01:00
this.emit('connectFailed');
return;
}
this.connectedDevice = device['device'];
await this.authenticate();
2020-01-17 14:50:58 +00:00
}
2020-01-20 10:58:03 +00:00
async onLastDataUpdated(iface, properties, invalidated) {
if (iface !== GATT_CHRC_ID) {
return;
}
2020-01-20 10:58:03 +00:00
const changedKeys = Object.keys(properties);
if (changedKeys.length === 0) {
return;
}
2020-01-20 10:58:03 +00:00
const value = await properties['Value'];
if (!value) {
return;
}
const data = value.value;
2020-01-19 20:08:48 +00:00
const decoded = this._encryptDecrypt(this.cryptoKey, this.plejdService.addr, data);
2020-01-17 14:50:58 +00:00
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'));
2020-01-17 14:50:58 +00:00
}
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;
2020-06-12 11:15:00 +02:00
this.plejdDevices[device] = {
state: state,
dim: dim
};
2020-01-17 14:50:58 +00:00
logger('d: ' + device + ' got state+dim update: ' + state + ' - ' + dim);
2020-06-12 11:15:00 +02:00
this.emit('stateChanged', device, {
state: state,
brightness: dim
});
return;
} else if (cmd === BLE_CMD_STATE_CHANGE) {
2020-01-17 14:50:58 +00:00
state = parseInt(decoded.toString('hex', 5, 6), 10);
logger('d: ' + device + ' got state update: ' + state);
2020-06-12 11:15:00 +02:00
this.emit('stateChanged', device, {
state: state
});
} else if (cmd === BLE_CMD_SCENE_TRIG) {
2020-01-17 14:50:58 +00:00
const scene = parseInt(decoded.toString('hex', 5, 6), 10);
this.emit('sceneTriggered', device, scene);
}
this.plejdDevices[device] = {
2020-06-12 11:16:48 +02:00
state: state,
dim: 0
2020-01-17 14:50:58 +00:00
};
}
wireEvents() {
console.log('wireEvents()');
const self = this;
this.on('pingFailed', this.onPingFailed.bind(self));
this.on('pingSuccess', this.onPingSuccess.bind(self));
}
_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, '');
2020-01-17 14:50:58 +00:00
cipher.setAutoPadding(false);
var ct = cipher.update(buf).toString('hex');
ct += cipher.final().toString('hex');
ct = Buffer.from(ct, 'hex');
var output = '';
2020-01-17 14:50:58 +00:00
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
}
}
2020-06-12 11:15:00 +02:00
module.exports = PlejdService;