UNPKG

@showbridge/lib

Version:

Main library for showbridge protocol router

260 lines (259 loc) 10.5 kB
import { isEqual } from 'lodash-es'; import { EventEmitter } from 'node:events'; import { WebSocket } from 'ws'; import Config from './config.js'; import { disabled, logger } from './utils/index.js'; import { ActionTypeClassMap } from './actions/index.js'; import { CloudProtocol, HTTPProtocol, MIDIProtocol, MQTTProtocol, TCPProtocol, UDPProtocol, } from './protocols/index.js'; class Router extends EventEmitter { constructor(config) { super(); if (!(config instanceof Config)) { throw new Error('router config is not an instance of Config'); } this.vars = {}; this._config = config; this.protocols = { http: new HTTPProtocol(this.config.protocols.http, this), udp: new UDPProtocol(this.config.protocols.udp, this), tcp: new TCPProtocol(this.config.protocols.tcp, this), midi: new MIDIProtocol(this.config.protocols.midi, this), mqtt: new MQTTProtocol(this.config.protocols.mqtt, this), cloud: new CloudProtocol(this.config.protocols.cloud, this), }; // NOTE(jwetzell): listen for all messages on all protocols Object.keys(this.protocols).forEach((protocol) => { this.protocols[protocol].on('messageIn', (msg) => { this.processMessage(msg); }); }); this.protocols.http.on('configUploaded', (updatedConfig) => { try { this.config = updatedConfig; this.emit('configUpdated', updatedConfig); logger.debug('router: config updated successfully'); } catch (error) { logger.error(error); logger.error('router: problem applying new config'); } }); this.protocols.http.on('runAction', (action, msg, vars) => { this.runAction(action, msg, vars); }); this.protocols.http.on('getProtocolStatus', (webSocket) => { const protocolStatusEvent = { eventName: 'protocolStatus', data: {}, }; try { Object.keys(this.protocols).forEach((protocol) => { protocolStatusEvent.data[protocol] = this.protocols[protocol].status; }); if (webSocket.readyState === WebSocket.OPEN) { webSocket.send(JSON.stringify(protocolStatusEvent)); } } catch (error) { logger.error(`router: problem sending protocolStatus to websocket`); logger.error(error); } }); this.on('trigger', (triggerEvent) => { this.protocols.http.sendToWebUISockets('trigger', triggerEvent); }); this.on('action', (actionEvent) => { if (actionEvent.action?.type === 'store') { this.emit('varsUpdated', this.vars); } this.protocols.http.sendToWebUISockets('action', actionEvent); }); this.on('transform', (transformEvent) => { this.protocols.http.sendToWebUISockets('transform', transformEvent); }); } set config(value) { const protocolsToReload = []; Object.keys(this.protocols).forEach((protocol) => { if (value.protocols[protocol].params) { if (!isEqual(value.protocols[protocol].params, this.config.protocols[protocol].params)) { logger.debug(`router: ${protocol} config has changed and marked for reload`); protocolsToReload.push(protocol); } } }); this._config = value; protocolsToReload.forEach((protocol) => { this.reloadProtocol(protocol); }); } get config() { return this._config; } stop() { let protocolsClosed = 0; logger.info('router: waiting for all protocols to say they have stopped'); const protocolKeys = Object.keys(this.protocols); protocolKeys.forEach((protocol) => { this.protocols[protocol].on('stopped', () => { protocolsClosed += 1; logger.trace(`router: protocol ${protocol} has stopped`); if (protocolsClosed === protocolKeys.length) { logger.info('router: all protocols have stopped'); this.emit('stopped'); } }); this.stopProtocol(protocol); }); // NOTE(jwetzell): if protocols haven't stopped in a reasonable time just stop setTimeout(() => { logger.warn('router: protocols have taken a while to stop'); this.emit('stopped'); }, 2000); } start() { Object.keys(this.protocols).forEach((protocol) => { this.protocols[protocol].on('started', () => { this.emit('protocolStarted', protocol); }); try { this.reloadProtocol(protocol); } catch (error) { logger.error(`router: problem reloading ${protocol} protocol`); logger.error(error); } }); } setLogLevel(logLevel) { logger.level = logLevel; } servePath(filePath) { this.protocols.http.servePath(filePath); } processTrigger(trigger, triggerPath, msg) { try { const triggerShouldFire = trigger.shouldFire(msg, this.vars); const triggerEventObj = { path: triggerPath, fired: triggerShouldFire, }; logger.trace(`trigger: ${triggerEventObj.path}: ${triggerShouldFire ? 'fired' : 'skipped'}`); if (triggerShouldFire) { trigger.actions?.forEach((action, actionIndex) => { const actionEventObj = { action: action, path: `${triggerEventObj.path}/actions/${actionIndex}`, fired: action.enabled, }; logger.trace(`action: ${actionEventObj.path}: ${actionEventObj.fired ? 'fired' : 'skipped'}`); try { // NOTE(jwetzell): listen for subaction events and bubble them up action.on('transform', (transformPath, enabled) => { const transformEventObj = { path: `${actionEventObj.path}/${transformPath}`, fired: enabled, }; logger.trace(`transform: ${transformEventObj.path}: ${transformEventObj.fired ? 'fired' : 'skipped'}`); this.emit('transform', transformEventObj); }); // NOTE(jwetzell): listen for subaction events and bubble them up action.on('action', (subAction, subActionPath, enabled) => { const subActionEventObject = { action: subAction, path: `${actionEventObj.path}/${subActionPath}`, fired: enabled, }; this.emit('action', subActionEventObject); }); // NOTE(jwetzell): clean up listeners action.once('finished', () => { action.removeAllListeners('action'); action.removeAllListeners('transform'); }); // NOTE(jwetzell): perform the action action.run(msg, this.vars, this.protocols); this.emit('action', actionEventObj); } catch (error) { logger.error(`action: problem running action - ${error}`); } }); if (trigger.subTriggers) { trigger.subTriggers.forEach((subTrigger, subTriggerIndex) => { this.processTrigger(subTrigger, `${triggerPath}/subTriggers/${subTriggerIndex}`, msg); }); } } this.emit('trigger', triggerEventObj); } catch (error) { logger.error(`trigger: problem evaluating trigger - ${error}`); } } processMessage(msg) { const messageEventObj = { type: msg.messageType, }; this.emit('messageIn', messageEventObj); const triggers = this.config.getTriggers(msg.messageType); if (triggers !== undefined && triggers.length > 0) { triggers.forEach((trigger, triggerIndex) => { this.processTrigger(trigger, `${msg.messageType}/triggers/${triggerIndex}`, msg); }); } } runAction(action, msg, vars) { try { const onDemandAction = new ActionTypeClassMap[action.type](action); onDemandAction.run(msg, vars || this.vars, this.protocols); } catch (error) { logger.error('router: problem running action on demand'); logger.error(error); } } stopProtocol(type) { if (this.protocols[type]) { this.protocols[type].stop(); } } reloadProtocol(type) { logger.trace(`router: reloading ${type} protocol`); if (this.config.protocols[type] && !disabled.protocols.has(type)) { if (this.config.protocols[type].params) { this.protocols[type].reload(this.config.protocols[type].params); } else { this.protocols[type].reload(); } } } disableAction(type) { disabled.actions.add(type); } enableAction(type) { disabled.actions.delete(type); } disableProtocol(type) { disabled.protocols.add(type); this.stopProtocol(type); } enabledProtocol(type) { disabled.protocols.delete(type); this.reloadProtocol(type); } disableTrigger(type) { disabled.triggers.add(type); } enableTrigger(type) { disabled.triggers.delete(type); } disableTransform(type) { disabled.transforms.add(type); } enableTransform(type) { disabled.transforms.delete(type); } } export default Router;