const {InstanceBase, Regex, runEntrypoint, InstanceStatus, combineRgb} = require('@companion-module/base') const UpgradeScripts = require('./upgrades') const UpdateActions = require('./actions') const UpdateFeedbacks = require('./feedbacks') const WebSocket = require('ws') const ProjectUpdate = require("./receive/project_update"); const Project = require("./project"); const PadNameUpdate = require("./receive/pad_name_update"); const PadStatusUpdate = require("./receive/pad_status_update"); const CurrentPageUpdate = require("./receive/current_page_update"); const CurrentPageRequest = require("./receive/current_page_request"); const uuid = require('uuid'); const presets = require('./presets'); class ModuleInstance extends InstanceBase { isInitialized = false // language=RegExp wsRegex = '^wss?:\\/\\/([\\da-z.-]+)(:\\d{1,5})?(?:\\/(.*))?$' messageHandlers = { 'project-current': new ProjectUpdate(), 'pad-name-changed': new PadNameUpdate(), 'pad-status-changed': new PadStatusUpdate(), 'current-page-changed': new CurrentPageUpdate(), 'current-page-request': new CurrentPageRequest() }; currentProject = new Project({}); constructor(internal) { super(internal) } async init(config) { this.config = config this.initWebSocket() this.isInitialized = true this.updateActions() await this.updateFeedbacks() this.setPresetDefinitions(presets()); } // When module gets deleted async destroy() { this.isInitialized = false if (this.reconnect_timer) { clearTimeout(this.reconnect_timer) this.reconnect_timer = null } if (this.ws) { this.ws.close(1000) delete this.ws } } async configUpdated(config) { this.config = config this.initWebSocket() } // Return config fields for web config getConfigFields() { return [ { type: 'textinput', id: 'host', label: 'PlayWall IP', default: '127.0.0.1', width: 8, regex: Regex.IP, }, { type: 'textinput', id: 'port', label: 'PlayWall Port', default: '9876', width: 4, regex: Regex.PORT, }, { type: 'checkbox', id: 'debug_messages', label: 'Debug messages', tooltip: 'Log incomming and outcomming messages', width: 6, }, { type: 'checkbox', id: 'reconnect', label: 'Reconnect', tooltip: 'Reconnect on WebSocket error (after 5 secs)', width: 6, default: true, }, ] } updateActions() { UpdateActions(this) } async updateFeedbacks() { await UpdateFeedbacks(this) } // Websocket handling maybeReconnect() { if (this.isInitialized && this.config.reconnect) { if (this.reconnect_timer) { clearTimeout(this.reconnect_timer) } this.reconnect_timer = setTimeout(() => { this.initWebSocket() }, 1000) } } initWebSocket() { if (this.reconnect_timer) { clearTimeout(this.reconnect_timer) this.reconnect_timer = null } if (this.config.host == null || this.config.port == null) { this.log('debug', `PlayWall host '${this.config.host}' or port '${this.config.port}' is invalid`); this.updateStatus(InstanceStatus.BadConfig, `PlayWall host '${this.config.host}' or port '${this.config.port}' is invalid`) return } const url = `ws://${this.config.host}:${this.config.port}/api` if (!url || url.match(new RegExp(this.wsRegex)) === null) { this.updateStatus(InstanceStatus.BadConfig, `WS URL is not defined or invalid`) return } this.updateStatus(InstanceStatus.Connecting) if (this.ws) { this.ws.close(1000) delete this.ws } this.ws = new WebSocket(url) this.ws.on('open', () => { this.updateStatus(InstanceStatus.Ok) this.log('debug', `Connection opened`) this.sendToWebSocket('project-current', {}); }) this.ws.on('close', (code) => { this.log('debug', `Connection closed with code ${code}`) this.updateStatus(InstanceStatus.Disconnected, `Connection closed with code ${code}`) this.maybeReconnect(); }) this.ws.on('message', this.messageReceivedFromWebSocket.bind(this)) this.ws.on('error', (data) => { this.log('error', `WebSocket error: ${data}`) }) } sendToWebSocket(type, payload) { this.ws.send(JSON.stringify({ 'type': type, 'messageId': uuid.v4(), 'payload': payload })); } messageReceivedFromWebSocket(data) { if (this.config.debug_messages) { this.log('debug', `Message received: ${data}`) } const message = JSON.parse(data) if (message.type != null) { if (this.messageHandlers[message.type] != null) { this.messageHandlers[message.type].handleMessage(this, message) } else { this.log('debug', `Cannot handle incoming message of type ${message.type}`) } } else if (message.updateType != null) { if (this.messageHandlers[message.updateType] != null) { this.messageHandlers[message.updateType].handleMessage(this, message) } else { this.log('debug', `Cannot handle incoming message of updateType ${message.updateType}`) } } else { this.log('debug', `Cannot handle incoming message ${data}`) } } updateVariables() { this.setVariableDefinitions([...Array(this.currentProject.getPadCount()).keys()].map(index => { return {variableId: `pad-${index}`, name: `Pad Index ${index + 1}`} })) } updateVariablesForCurrentPage() { let pads = this.currentProject.getAllPadsOfCurrentPage(); for (let i in pads) { let pad = pads[i]; this.log('debug', `Update variable 'pad-${pad.position}'`); this.setVariableValues({[`pad-${pad.position}`]: pad.name}); } } } runEntrypoint(ModuleInstance, UpgradeScripts)