diff --git a/plejd/DeviceRegistry.js b/plejd/DeviceRegistry.js index 0a60c3d..e9c45c8 100644 --- a/plejd/DeviceRegistry.js +++ b/plejd/DeviceRegistry.js @@ -34,14 +34,20 @@ class DeviceRegistry { this.deviceIdsBySerial[added.serialNumber] = added.id; - logger.verbose(`Added/updated device: ${JSON.stringify(added)}`); + logger.verbose( + `Added/updated device: ${JSON.stringify(added)}. ${ + Object.keys(this.plejdDevices).length + } plejd devices in total.`, + ); if (added.roomId) { this.deviceIdsByRoom[added.roomId] = [ ...(this.deviceIdsByRoom[added.roomId] || []), added.id, ]; - logger.verbose(`Added to room: ${JSON.stringify(this.deviceIdsByRoom[added.roomId])}`); + logger.verbose( + `Added to room #${added.roomId}: ${JSON.stringify(this.deviceIdsByRoom[added.roomId])}`, + ); } return added; @@ -54,10 +60,14 @@ class DeviceRegistry { }; this.roomDevices = { ...this.roomDevices, - [device.id]: added, + [added.id]: added, }; - logger.verbose(`Added/updated room device: ${JSON.stringify(added)}`); + logger.verbose( + `Added/updated room device: ${JSON.stringify(added)}. ${ + Object.keys(this.roomDevices).length + } room devices total.`, + ); return added; } @@ -68,9 +78,13 @@ class DeviceRegistry { }; this.sceneDevices = { ...this.sceneDevices, - added, + [added.id]: added, }; - logger.verbose(`Added/updated scene: ${JSON.stringify(added)}`); + logger.verbose( + `Added/updated scene: ${JSON.stringify(added)}. ${ + Object.keys(this.sceneDevices).length + } scenes in total.`, + ); return added; } @@ -126,7 +140,8 @@ class DeviceRegistry { } setState(deviceId, state, dim) { - const device = this.addPlejdDevice({ id: deviceId, state }); + const device = this.getDevice(deviceId) || this.addPlejdDevice({ id: deviceId }); + device.state = state; if (dim && device.dimmable) { device.dim = dim; } diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index fa05a9d..3571951 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -70,6 +70,11 @@ const getSwitchPayload = (device) => ({ class MqttClient extends EventEmitter { deviceRegistry; + static EVENTS = { + connected: 'connected', + stateChanged: 'stateChanged', + }; + constructor(deviceRegistry) { super(); @@ -97,7 +102,7 @@ class MqttClient extends EventEmitter { logger.error('Unable to subscribe to status topics'); } - this.emit('connected'); + this.emit(MqttClient.EVENTS.connected); }); this.client.subscribe(getSubscribePath(), (err) => { @@ -115,7 +120,7 @@ class MqttClient extends EventEmitter { this.client.on('message', (topic, message) => { if (startTopics.includes(topic)) { logger.info('Home Assistant has started. lets do discovery.'); - this.emit('connected'); + this.emit(MqttClient.EVENTS.connected); } else { const decodedTopic = decodeTopic(topic); if (decodedTopic) { @@ -147,7 +152,7 @@ class MqttClient extends EventEmitter { ); if (device) { - this.emit('stateChanged', device, command); + this.emit(MqttClient.EVENTS.stateChanged, device, command); } else { logger.warn( `Device for topic ${topic} not found! Can happen if HA calls previously existing devices.`, @@ -181,6 +186,10 @@ class MqttClient extends EventEmitter { this.client.reconnect(); } + cleanup() { + this.client.removeAllListeners(); + } + disconnect(callback) { this.deviceRegistry.allDevices.forEach((device) => { this.client.publish(getAvailabilityTopic(device), 'offline'); diff --git a/plejd/PlejdAddon.js b/plejd/PlejdAddon.js index 2fc9c72..0f3c6f2 100644 --- a/plejd/PlejdAddon.js +++ b/plejd/PlejdAddon.js @@ -17,6 +17,7 @@ class PlejdAddon extends EventEmitter { plejdApi; plejdDeviceCommunication; mqttClient; + processCleanupFunc; sceneManager; constructor() { @@ -31,19 +32,30 @@ class PlejdAddon extends EventEmitter { this.mqttClient = new MqttClient(this.deviceRegistry); } + cleanup() { + this.mqttClient.cleanup(); + this.mqttClient.removeAllListeners(); + this.plejdDeviceCommunication.cleanup(); + this.plejdDeviceCommunication.removeAllListeners(); + } + async init() { logger.info('Main Plejd addon init()...'); await this.plejdApi.init(); this.sceneManager.init(); + this.processCleanupFunc = () => { + this.cleanup(); + this.processCleanupFunc = () => {}; + this.mqttClient.disconnect(() => process.exit(0)); + }; + ['SIGINT', 'SIGHUP', 'SIGTERM'].forEach((signal) => { - process.on(signal, () => { - this.mqttClient.disconnect(() => process.exit(0)); - }); + process.on(signal, this.processCleanupFunc); }); - this.mqttClient.on('connected', () => { + this.mqttClient.on(MqttClient.EVENTS.connected, () => { try { logger.verbose('connected to mqtt.'); this.mqttClient.sendDiscoveryToHomeAssistant(); @@ -53,7 +65,7 @@ class PlejdAddon extends EventEmitter { }); // subscribe to changes from HA - this.mqttClient.on('stateChanged', (device, command) => { + this.mqttClient.on(MqttClient.EVENTS.stateChanged, (device, command) => { try { const deviceId = device.id; @@ -99,21 +111,27 @@ class PlejdAddon extends EventEmitter { this.mqttClient.init(); // subscribe to changes from Plejd - this.plejdDeviceCommunication.on('stateChanged', (deviceId, command) => { - try { - this.mqttClient.updateState(deviceId, command); - } catch (err) { - logger.error('Error in PlejdService.stateChanged callback in main.js', err); - } - }); + this.plejdDeviceCommunication.on( + PlejdDeviceCommunication.EVENTS.stateChanged, + (deviceId, command) => { + try { + this.mqttClient.updateState(deviceId, command); + } catch (err) { + logger.error('Error in PlejdService.stateChanged callback in main.js', err); + } + }, + ); - this.plejdDeviceCommunication.on('sceneTriggered', (deviceId, sceneId) => { - try { - this.mqttClient.sceneTriggered(sceneId); - } catch (err) { - logger.error('Error in PlejdService.sceneTriggered callback in main.js', err); - } - }); + this.plejdDeviceCommunication.on( + PlejdDeviceCommunication.EVENTS.sceneTriggered, + (deviceId, sceneId) => { + try { + this.mqttClient.sceneTriggered(sceneId); + } catch (err) { + logger.error('Error in PlejdService.sceneTriggered callback in main.js', err); + } + }, + ); await this.plejdDeviceCommunication.init(); logger.info('Main init done'); diff --git a/plejd/PlejdBLEHandler.js b/plejd/PlejdBLEHandler.js index 92b1232..195c627 100644 --- a/plejd/PlejdBLEHandler.js +++ b/plejd/PlejdBLEHandler.js @@ -62,6 +62,8 @@ class PlejBLEHandler extends EventEmitter { connected: 'connected', reconnecting: 'reconnecting', commandReceived: 'commandReceived', + writeFailed: 'writeFailed', + writeSuccess: 'writeSuccess', }; constructor(deviceRegistry) { @@ -80,14 +82,30 @@ class PlejBLEHandler extends EventEmitter { auth: null, ping: null, }; + } - this.on('writeFailed', (error) => this._onWriteFailed(error)); - this.on('writeSuccess', () => this._onWriteSuccess()); + cleanup() { + this.removeAllListeners(PlejBLEHandler.EVENTS.writeFailed); + this.removeAllListeners(PlejBLEHandler.EVENTS.writeSuccess); + + if (this.bus) { + this.bus.removeAllListeners('error'); + this.bus.removeAllListeners('connect'); + } + if (this.characteristics.lastDataProperties) { + this.characteristics.lastDataProperties.removeAllListeners('PropertiesChanged'); + } + if (this.objectManager) { + this.objectManager.removeAllListeners('InterfacesAdded'); + } } async init() { logger.info('init()'); + this.on(PlejBLEHandler.EVENTS.writeFailed, (error) => this._onWriteFailed(error)); + this.on(PlejBLEHandler.EVENTS.writeSuccess, () => this._onWriteSuccess()); + this.bus = dbus.systemBus(); this.bus.on('error', (err) => { // Uncaught error events will show UnhandledPromiseRejection logs @@ -276,6 +294,7 @@ class PlejBLEHandler extends EventEmitter { async _getInterface() { const bluez = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, '/'); + this.objectManager = await bluez.getInterface(DBUS_OM_INTERFACE); // We need to find the ble interface which implements the Adapter1 interface @@ -404,13 +423,12 @@ class PlejBLEHandler extends EventEmitter { async _onInterfacesAdded(path, interfaces) { logger.silly(`Interface added ${path}, inspecting...`); - // const [adapter, dev, service, characteristic] = path.split('/').slice(3); const interfaceKeys = Object.keys(interfaces); if (interfaceKeys.indexOf(BLUEZ_DEVICE_ID) > -1) { if (interfaces[BLUEZ_DEVICE_ID].UUIDs.value.indexOf(PLEJD_SERVICE) > -1) { logger.debug(`Found Plejd service on ${path}`); - + this.objectManager.removeAllListeners('InterfacesAdded'); await this._initDiscoveredPlejdDevice(path); } else { logger.error('Uh oh, no Plejd device!'); @@ -452,6 +470,7 @@ class PlejBLEHandler extends EventEmitter { // eslint-disable-next-line no-constant-condition while (true) { try { + this.cleanup(); await delay(5000); this.emit(PlejBLEHandler.EVENTS.reconnecting); logger.info('Reconnecting BLE...'); diff --git a/plejd/PlejdDeviceCommunication.js b/plejd/PlejdDeviceCommunication.js index 9ce469c..6497e9b 100644 --- a/plejd/PlejdDeviceCommunication.js +++ b/plejd/PlejdDeviceCommunication.js @@ -30,22 +30,31 @@ class PlejdDeviceCommunication extends EventEmitter { this.plejdBleHandler = new PlejBLEHandler(deviceRegistry); this.config = Configuration.getOptions(); this.deviceRegistry = deviceRegistry; + } - // eslint-disable-next-line max-len - this.plejdBleHandler.on(PlejBLEHandler.EVENTS.commandReceived, (deviceId, command, data) => this._bleCommandReceived(deviceId, command, data)); - - this.plejdBleHandler.on('connected', () => { - logger.info('Bluetooth connected. Plejd BLE up and running!'); - this.startWriteQueue(); - }); - this.plejdBleHandler.on('reconnecting', () => { - logger.info('Bluetooth reconnecting...'); - clearTimeout(this.writeQueueRef); - }); + cleanup() { + this.plejdBleHandler.cleanup(); + this.plejdBleHandler.removeAllListeners(PlejBLEHandler.EVENTS.commandReceived); + this.plejdBleHandler.removeAllListeners(PlejBLEHandler.EVENTS.connected); + this.plejdBleHandler.removeAllListeners(PlejBLEHandler.EVENTS.reconnecting); } async init() { try { + // eslint-disable-next-line max-len + this.plejdBleHandler.on(PlejBLEHandler.EVENTS.commandReceived, (deviceId, command, data) => this._bleCommandReceived(deviceId, command, data)); + + this.plejdBleHandler.on(PlejBLEHandler.EVENTS.connected, () => { + logger.info('Bluetooth connected. Plejd BLE up and running!'); + logger.verbose('Starting writeQueue loop.'); + this.startWriteQueue(); + }); + this.plejdBleHandler.on(PlejBLEHandler.EVENTS.reconnecting, () => { + logger.info('Bluetooth reconnecting...'); + logger.verbose('Stopping writeQueue loop until connection is established.'); + clearTimeout(this.writeQueueRef); + }); + await this.plejdBleHandler.init(); } catch (err) { logger.error('Failed init() of BLE. Starting reconnect loop.'); @@ -108,9 +117,8 @@ class PlejdDeviceCommunication extends EventEmitter { } _transitionTo(deviceId, targetBrightness, transition, deviceName) { - const initialBrightness = this.plejdDevices[deviceId] - ? this.plejdDevices[deviceId].state && this.plejdDevices[deviceId].dim - : null; + const device = this.deviceRegistry.getDevice(deviceId); + const initialBrightness = device ? device.state && device.dim : null; this._clearDeviceTransitionTimer(deviceId); const isDimmable = this.deviceRegistry.getDevice(deviceId).dimmable; diff --git a/plejd/SceneManager.js b/plejd/SceneManager.js index a30e58b..e5047cc 100644 --- a/plejd/SceneManager.js +++ b/plejd/SceneManager.js @@ -1,16 +1,13 @@ -const EventEmitter = require('events'); const Logger = require('./Logger'); const Scene = require('./Scene'); const logger = Logger.getLogger('scene-manager'); -class SceneManager extends EventEmitter { +class SceneManager { deviceRegistry; plejdBle; scenes; constructor(deviceRegistry, plejdBle) { - super(); - this.deviceRegistry = deviceRegistry; this.plejdBle = plejdBle; this.scenes = {};