@showbridge/lib
Version:
Main library for showbridge protocol router
260 lines (259 loc) • 10.5 kB
JavaScript
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;