Restructure PlejdBLE slightly to async/await and clarify flow by logging

This commit is contained in:
Victor Hagelbäck 2021-02-01 21:29:44 +01:00
parent c5ee71d503
commit 62a6359544
2 changed files with 110 additions and 82 deletions

View file

@ -4,6 +4,8 @@ const xor = require('buffer-xor');
const EventEmitter = require('events'); const EventEmitter = require('events');
const Logger = require('./Logger'); const Logger = require('./Logger');
const Configuration = require('./Configuration');
const logger = Logger.getLogger('plejd-ble'); const logger = Logger.getLogger('plejd-ble');
// UUIDs // UUIDs
@ -30,24 +32,26 @@ const GATT_CHRC_ID = 'org.bluez.GattCharacteristic1';
const MAX_TRANSITION_STEPS_PER_SECOND = 5; // Could be made a setting const MAX_TRANSITION_STEPS_PER_SECOND = 5; // Could be made a setting
const MAX_RETRY_COUNT = 5; // Could be made a setting const MAX_RETRY_COUNT = 5; // Could be made a setting
class PlejdService extends EventEmitter { const delay = (timeout) => new Promise((resolve) => setTimeout(resolve, timeout));
constructor(cryptoKey, devices, sceneManager, connectionTimeout, writeQueueWaitTime) {
class PlejBLE extends EventEmitter {
config;
deviceRegistry;
constructor(deviceRegistry) {
super(); super();
logger.info('Starting Plejd BLE, resetting all device states.'); logger.info('Starting Plejd BLE, resetting all device states.');
this.cryptoKey = Buffer.from(cryptoKey.replace(/-/g, ''), 'hex'); this.config = Configuration.getOptions();
this.deviceRegistry = deviceRegistry;
this.sceneManager = sceneManager;
this.connectedDevice = null; this.connectedDevice = null;
this.plejdService = null; this.plejdService = null;
this.bleDevices = []; this.bleDevices = [];
this.bleDeviceTransitionTimers = {}; this.bleDeviceTransitionTimers = {};
this.plejdDevices = {}; this.plejdDevices = {};
this.devices = devices;
this.connectEventHooked = false; this.connectEventHooked = false;
this.connectionTimeout = connectionTimeout;
this.writeQueueWaitTime = writeQueueWaitTime;
this.writeQueue = []; this.writeQueue = [];
this.writeQueueRef = null; this.writeQueueRef = null;
this.initInProgress = null; this.initInProgress = null;
@ -61,14 +65,19 @@ class PlejdService extends EventEmitter {
ping: null, ping: null,
}; };
this.bus = dbus.systemBus(); this.bus = null;
this.adapter = null; this.adapter = null;
logger.debug('wiring events and waiting for BLE interface to power up.');
this.wireEvents();
} }
async init() { async init() {
logger.info('init()');
this.bus = dbus.systemBus();
logger.debug('wiring events and waiting for BLE interface to power up.');
this.wireEvents();
this.cryptoKey = Buffer.from(this.deviceRegistry.cryptoKey.replace(/-/g, ''), 'hex');
if (this.objectManager) { if (this.objectManager) {
this.objectManager.removeAllListeners(); this.objectManager.removeAllListeners();
} }
@ -86,7 +95,6 @@ class PlejdService extends EventEmitter {
clearInterval(this.pingRef); clearInterval(this.pingRef);
clearTimeout(this.writeQueueRef); clearTimeout(this.writeQueueRef);
logger.info('init()');
const bluez = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, '/'); const bluez = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, '/');
this.objectManager = await bluez.getInterface(DBUS_OM_INTERFACE); this.objectManager = await bluez.getInterface(DBUS_OM_INTERFACE);
@ -101,31 +109,49 @@ class PlejdService extends EventEmitter {
if (!this.adapter) { if (!this.adapter) {
logger.error('Unable to find a bluetooth adapter that is compatible.'); logger.error('Unable to find a bluetooth adapter that is compatible.');
return Promise.reject(new Error('Unable to find a bluetooth adapter that is compatible.')); throw new Error('Unable to find a bluetooth adapter that is compatible.');
} }
logger.verbose(
`Iterating ${
Object.keys(managedObjects).length
} BLE managedObjects looking for ${BLUEZ_DEVICE_ID}`,
);
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (const path of Object.keys(managedObjects)) { for (const path of Object.keys(managedObjects)) {
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
try {
const interfaces = Object.keys(managedObjects[path]); const interfaces = Object.keys(managedObjects[path]);
if (interfaces.indexOf(BLUEZ_DEVICE_ID) > -1) { if (interfaces.indexOf(BLUEZ_DEVICE_ID) > -1) {
const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path); const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path);
const device = await proxyObject.getInterface(BLUEZ_DEVICE_ID); const device = await proxyObject.getInterface(BLUEZ_DEVICE_ID);
logger.verbose(`Found ${path} - ${JSON.stringify(device.device)}`);
const connected = managedObjects[path][BLUEZ_DEVICE_ID].Connected.value; const connected = managedObjects[path][BLUEZ_DEVICE_ID].Connected.value;
if (connected) { if (connected) {
logger.info(`disconnecting ${path}`); logger.info(`disconnecting ${path}. This can take up to 180 seconds`);
await device.Disconnect(); await device.Disconnect();
} }
logger.verbose(`Removing ${path} from adapter.`);
await this.adapter.RemoveDevice(path); await this.adapter.RemoveDevice(path);
} }
} catch (err) {
logger.error(`Error handling ${path}`, err);
}
/* eslint-enable no-await-in-loop */ /* eslint-enable no-await-in-loop */
} }
this.objectManager.on('InterfacesAdded', this.onInterfacesAdded.bind(this)); logger.verbose('All active BLE device connections cleaned up.');
logger.verbose('Setting up interfacesAdded subscription and discovery filter');
this.objectManager.on('InterfacesAdded', (path, interfaces) =>
this.onInterfacesAdded(path, interfaces),
);
this.adapter.SetDiscoveryFilter({ this.adapter.SetDiscoveryFilter({
UUIDs: new dbus.Variant('as', [PLEJD_SERVICE]), UUIDs: new dbus.Variant('as', [PLEJD_SERVICE]),
@ -133,26 +159,33 @@ class PlejdService extends EventEmitter {
}); });
try { try {
logger.verbose('Starting BLE discovery... This can take up to 180 seconds.');
await this.adapter.StartDiscovery(); await this.adapter.StartDiscovery();
logger.verbose('Started BLE discovery');
} catch (err) { } catch (err) {
logger.error('Failed to start discovery. Make sure no other add-on is currently scanning.'); logger.error('Failed to start discovery.', err);
return Promise.reject( if (err.message.includes('Operation already in progress')) {
new Error('Failed to start discovery. Make sure no other add-on is currently scanning.'), logger.info(
'If you continue to get "operation already in progress" error, you can try power cycling the bluetooth adapter. Get root console access, run "bluetoothctl" => "power off" => "power on" => "exit" => restart addon.',
); );
} }
return new Promise((resolve) => setTimeout( throw new Error(
() => resolve( 'Failed to start discovery. Make sure no other add-on is currently scanning.',
this._internalInit().catch((err) => { );
logger.error('InternalInit exception! Will rethrow.', err); }
throw err;
}), await delay(this.config.connectionTimeout * 1000);
),
this.connectionTimeout * 1000, await this._internalInit();
));
logger.info('Init done');
} }
async _internalInit() { async _internalInit() {
logger.debug(`Got ${this.bleDevices.length} device(s).`); logger.debug(`InternalInit(). Got ${this.bleDevices.length} device(s).`);
if (this.bleDevices.length === 0) {
logger.warn('No devices, init will presumably not work');
}
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (const plejd of this.bleDevices) { for (const plejd of this.bleDevices) {
@ -170,7 +203,7 @@ class PlejdService extends EventEmitter {
const segments = plejd.path.split('/'); const segments = plejd.path.split('/');
let fixedPlejdPath = segments[segments.length - 1].replace('dev_', ''); let fixedPlejdPath = segments[segments.length - 1].replace('dev_', '');
fixedPlejdPath = fixedPlejdPath.replace(/_/g, ''); fixedPlejdPath = fixedPlejdPath.replace(/_/g, '');
plejd.device = this.devices.find((x) => x.serialNumber === fixedPlejdPath); plejd.device = this.deviceRegistry.getDeviceBySerialNumber(fixedPlejdPath);
logger.debug(`Discovered ${plejd.path} with rssi ${plejd.rssi}`); logger.debug(`Discovered ${plejd.path} with rssi ${plejd.rssi}`);
} catch (err) { } catch (err) {
@ -197,10 +230,12 @@ class PlejdService extends EventEmitter {
} }
} }
setTimeout(async () => { await delay(this.config.connectionTimeout * 1000);
await this.onDeviceConnected(connectedDevice); await this.onDeviceConnected(connectedDevice);
logger.verbose('Stopping discovery...');
await this.adapter.StopDiscovery(); await this.adapter.StopDiscovery();
}, this.connectionTimeout * 1000);
} }
async _getInterface(managedObjects, iface) { async _getInterface(managedObjects, iface) {
@ -225,6 +260,7 @@ class PlejdService extends EventEmitter {
} }
async onInterfacesAdded(path, interfaces) { async onInterfacesAdded(path, interfaces) {
logger.silly(`Interface added ${path}, inspecting...`);
// const [adapter, dev, service, characteristic] = path.split('/').slice(3); // const [adapter, dev, service, characteristic] = path.split('/').slice(3);
const interfaceKeys = Object.keys(interfaces); const interfaceKeys = Object.keys(interfaces);
@ -237,11 +273,13 @@ class PlejdService extends EventEmitter {
} else { } else {
logger.error('Uh oh, no Plejd device!'); logger.error('Uh oh, no Plejd device!');
} }
} else {
logger.silly('Not the right device id');
} }
} }
turnOn(deviceId, command) { turnOn(deviceId, command) {
const deviceName = this._getDeviceName(deviceId); const deviceName = this.deviceRegistry.getDeviceName(deviceId);
logger.info( logger.info(
`Plejd got turn on command for ${deviceName} (${deviceId}), brightness ${command.brightness}${ `Plejd got turn on command for ${deviceName} (${deviceId}), brightness ${command.brightness}${
command.transition ? `, transition: ${command.transition}` : '' command.transition ? `, transition: ${command.transition}` : ''
@ -251,7 +289,7 @@ class PlejdService extends EventEmitter {
} }
turnOff(deviceId, command) { turnOff(deviceId, command) {
const deviceName = this._getDeviceName(deviceId); const deviceName = this.deviceRegistry.getDeviceName(deviceId);
logger.info( logger.info(
`Plejd got turn off command for ${deviceName} (${deviceId})${ `Plejd got turn off command for ${deviceName} (${deviceId})${
command.transition ? `, transition: ${command.transition}` : '' command.transition ? `, transition: ${command.transition}` : ''
@ -272,14 +310,14 @@ class PlejdService extends EventEmitter {
: null; : null;
this._clearDeviceTransitionTimer(deviceId); this._clearDeviceTransitionTimer(deviceId);
const isDimmable = this.devices.find((d) => d.id === deviceId).dimmable; const isDimmable = this.deviceRegistry.getDevice(deviceId).dimmable;
if ( if (
transition > 1 transition > 1 &&
&& isDimmable isDimmable &&
&& (initialBrightness || initialBrightness === 0) (initialBrightness || initialBrightness === 0) &&
&& (targetBrightness || targetBrightness === 0) (targetBrightness || targetBrightness === 0) &&
&& targetBrightness !== initialBrightness targetBrightness !== initialBrightness
) { ) {
// Transition time set, known initial and target brightness // Transition time set, known initial and target brightness
// Calculate transition interval time based on delta brightness and max steps per second // Calculate transition interval time based on delta brightness and max steps per second
@ -386,14 +424,6 @@ class PlejdService extends EventEmitter {
}); });
} }
triggerScene(sceneIndex) {
const sceneName = this._getDeviceName(sceneIndex);
logger.info(
`Triggering scene ${sceneName} (${sceneIndex}). Scene name might be misleading if there is a device with the same numeric id.`,
);
this.sceneManager.executeScene(sceneIndex, this);
}
async authenticate() { async authenticate() {
logger.info('authenticate()'); logger.info('authenticate()');
@ -429,14 +459,16 @@ class PlejdService extends EventEmitter {
); );
return this.initInProgress; return this.initInProgress;
} }
this.initInProgress = new Promise((resolve) => setTimeout(async () => { this.initInProgress = new Promise((resolve) =>
setTimeout(async () => {
const result = await this.init().catch((err) => { const result = await this.init().catch((err) => {
logger.error('TrottledInit exception calling init(). Will re-throw.', err); logger.error('TrottledInit exception calling init(). Will re-throw.', err);
throw err; throw err;
}); });
this.initInProgress = null; this.initInProgress = null;
resolve(result); resolve(result);
}, delay)); }, delay),
);
return this.initInProgress; return this.initInProgress;
} }
@ -457,7 +489,7 @@ class PlejdService extends EventEmitter {
} else { } else {
logger.debug('Write failed ', err); logger.debug('Write failed ', err);
} }
await this.throttledInit(this.connectionTimeout * 1000); await this.throttledInit(this.config.connectionTimeout * 1000);
return false; return false;
} }
} }
@ -516,22 +548,22 @@ class PlejdService extends EventEmitter {
logger.info('startWriteQueue()'); logger.info('startWriteQueue()');
clearTimeout(this.writeQueueRef); clearTimeout(this.writeQueueRef);
this.writeQueueRef = setTimeout(() => this.runWriteQueue(), this.writeQueueWaitTime); this.writeQueueRef = setTimeout(() => this.runWriteQueue(), this.config.writeQueueWaitTime);
} }
async runWriteQueue() { async runWriteQueue() {
try { try {
while (this.writeQueue.length > 0) { while (this.writeQueue.length > 0) {
const queueItem = this.writeQueue.pop(); const queueItem = this.writeQueue.pop();
const deviceName = this._getDeviceName(queueItem.deviceId); const deviceName = this.deviceRegistry.getDeviceName(queueItem.deviceId);
logger.debug( logger.debug(
`Write queue: Processing ${deviceName} (${queueItem.deviceId}). Command ${queueItem.log}. Total queue length: ${this.writeQueue.length}`, `Write queue: Processing ${deviceName} (${queueItem.deviceId}). Command ${queueItem.log}. Total queue length: ${this.writeQueue.length}`,
); );
if (this.writeQueue.some((item) => item.deviceId === queueItem.deviceId)) { if (this.writeQueue.some((item) => item.deviceId === queueItem.deviceId)) {
logger.verbose( logger.verbose(
`Skipping ${deviceName} (${queueItem.deviceId}) ` `Skipping ${deviceName} (${queueItem.deviceId}) ` +
+ `${queueItem.log} due to more recent command in queue.`, `${queueItem.log} due to more recent command in queue.`,
); );
// Skip commands if new ones exist for the same deviceId // Skip commands if new ones exist for the same deviceId
// still process all messages in order // still process all messages in order
@ -559,7 +591,7 @@ class PlejdService extends EventEmitter {
logger.error('Error in writeQueue loop, values probably not written to Plejd', e); logger.error('Error in writeQueue loop, values probably not written to Plejd', e);
} }
this.writeQueueRef = setTimeout(() => this.runWriteQueue(), this.writeQueueWaitTime); this.writeQueueRef = setTimeout(() => this.runWriteQueue(), this.config.writeQueueWaitTime);
} }
async _processPlejdService(path, characteristics) { async _processPlejdService(path, characteristics) {
@ -615,9 +647,10 @@ class PlejdService extends EventEmitter {
async onDeviceConnected(device) { async onDeviceConnected(device) {
logger.info('onDeviceConnected()'); logger.info('onDeviceConnected()');
logger.debug(`Device: ${device}`); logger.debug(`Device: ${JSON.stringify(device.device)}`);
if (!device) { if (!device) {
logger.error('Device is null. Should we break/return when this happens?'); logger.error('onDeviceConnected device is null. Returning.');
return;
} }
const objects = await this.objectManager.GetManagedObjects(); const objects = await this.objectManager.GetManagedObjects();
@ -724,7 +757,7 @@ class PlejdService extends EventEmitter {
state, state,
dim, dim,
}; };
logger.verbose(`All states: ${JSON.stringify(this.plejdDevices)}`); logger.verbose(`All states: ${JSON.stringify(this.plejdDevices, null, 2)}`);
} else if (cmd === BLE_CMD_STATE_CHANGE) { } else if (cmd === BLE_CMD_STATE_CHANGE) {
logger.debug(`${deviceName} (${deviceId}) got state update. S: ${state}`); logger.debug(`${deviceName} (${deviceId}) got state update. S: ${state}`);
this.emit('stateChanged', deviceId, { this.emit('stateChanged', deviceId, {
@ -794,10 +827,6 @@ class PlejdService extends EventEmitter {
return Buffer.from(output, 'ascii'); return Buffer.from(output, 'ascii');
} }
_getDeviceName(deviceId) {
return (this.devices.find((d) => d.id === deviceId) || {}).name;
}
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
_reverseBuffer(src) { _reverseBuffer(src) {
const buffer = Buffer.allocUnsafe(src.length); const buffer = Buffer.allocUnsafe(src.length);
@ -811,4 +840,4 @@ class PlejdService extends EventEmitter {
} }
} }
module.exports = PlejdService; module.exports = PlejBLE;

View file

@ -1,13 +1,12 @@
{ {
"dependencies": { "dependencies": {
"@abandonware/bluetooth-hci-socket": "0.5.3-3", "@abandonware/bluetooth-hci-socket": "~0.5.3-7",
"axios": "~0.21.1", "axios": "~0.21.1",
"buffer-xor": "~2.0.2", "buffer-xor": "~2.0.2",
"dbus-next": "~0.9.1", "dbus-next": "~0.9.1",
"fs": "0.0.1-security", "fs": "0.0.1-security",
"jspack": "~0.0.4", "jspack": "~0.0.4",
"mqtt": "~3.0.0", "mqtt": "~3.0.0",
"sleep": "~6.1.0",
"winston": "~3.3.3" "winston": "~3.3.3"
}, },
"devDependencies": { "devDependencies": {