UNPKG

iobroker.pjlink

Version:
752 lines (676 loc) 32.6 kB
/** * * iobroker pjlink Adapter * * Copyright (c) 2022-2023, Bannsaenger <bannsaenger@gmx.de> * * MIT License * * Created with @iobroker/create-adapter 2.1.1 * */ const utils = require('@iobroker/adapter-core'); const pjlink = require('./lib/pjlink.js'); // possible query types const queries = ['POWR', 'INPT', 'CLSS', 'AVMT', 'ERST', 'LAMP', 'INST', 'NAME', 'INF1', 'INF2', 'INFO']; /** Projector status constants Four possible power states: * 0 / pjlink.POWER.OFF * 1 / pjlink.POWER.ON * 2 / pjlink.POWER.COOLING_DOWN * 3 / pjlink.POWER.WARMING_UP */ /* ToDo: go back to pjlink library from github. package.json: , "pjlink": "Bannsaenger/node-pjlink" */ class Pjlink extends utils.Adapter { /** * @param {Partial<utils.AdapterOptions>} [options={}] */ constructor(options) { super({ ...options, name: 'pjlink', }); // register callback functions this.on('ready', this.onReady.bind(this)); this.on('stateChange', this.onStateChange.bind(this)); this.on('message', this.onMessage.bind(this)); this.on('unload', this.onUnload.bind(this)); // prepare global instance variables this.projector = undefined; this.connectedState = false; // true if connection to projector is established, will be reset on connection errors this.poweredOn = false; // true if the power state is 1 (Power ON), used for status queries for which power must be ON this.firstRunDone = false; // true if the first run (query status on adapter startup) is done this.firstRunPowered = false; // true if the first run (query status on adapter startup with power = ON) is done this.skippedShortCycles = -1; // number of skipped short cycles after power ON event. Will be set to the config value and decremented. -1 is expired this.statusQueryInfo = { // a place to store the different query levels 'startup': [], 'startupPowered': [], 'long': [], 'longPowered': [], 'short': [], 'shortPowered': [] }; this.numStatusQryForInfo = 1; // after this number of status query a information query will be processed this.actStatusQryForInfo = 0; // actual number of skipped status queries this.unavailableTime = false; // true if the projector send the error "unavailable time" for the first time this.timers = {}; // a place to store timers this.timers.reconnectDelay = undefined; this.timers.statusDelay = undefined; } /** * Is called when databases are connected and adapter received configuration. */ async onReady() { try { //const self = this; // Reset the connection indicator during startup this.setState('info.connection', false, true); this.numStatusQryForInfo = Math.floor(this.config.informationDelay / this.config.statusDelay); await this.buildStatusQueryInfo(); this.conOptions = { 'host': this.config.host || '127.0.0.1', 'port': this.config.port || 4352, 'password': this.config.password || null, 'timeout': this.config.socketTimeout || 800 }; // In order to get state updates, you need to subscribe to them. The following line adds a subscription for our variable we have created above. this.subscribeStates('power'); this.subscribeStates('input'); this.subscribeStates('setMute'); this.log.info(`PJLink connecting to host: ${this.conOptions.host}:${this.conOptions.port} (timeout: ${this.conOptions.timeout} ms), ${this.conOptions.password ? 'with password set' : 'with security disabled'}`); // instantiate connection object for the projector this.projector = new pjlink(this.conOptions); // try to communicate to the projector this.reconnectProjector(); } catch (err) { this.errorHandler(err, 'onReady'); } } /** * Called to build the status query array */ async buildStatusQueryInfo() { try { for (const query of queries) { const queryType = `queryType${query}`; const queryTypePwr = `queryOnlyPwr${query}`; switch (this.config[queryType]) { case 1: // startup if (this.config[queryTypePwr]) { // @ts-ignore this.statusQueryInfo.startupPowered.push(query); } else { // @ts-ignore this.statusQueryInfo.startup.push(query); } break; case 2: // short if (this.config[queryTypePwr]) { // @ts-ignore this.statusQueryInfo.shortPowered.push(query); } else { // @ts-ignore this.statusQueryInfo.short.push(query); } break; case 3: // long if (this.config[queryTypePwr]) { // @ts-ignore this.statusQueryInfo.longPowered.push(query); } else { // @ts-ignore this.statusQueryInfo.long.push(query); } break; default: // never and other break; } } this.log.debug(`Ready building statusQueryInfo: ${JSON.stringify(this.statusQueryInfo)}`); } catch (err) { this.errorHandler(err, 'buildStatusQueryInfo'); } } /** * Called to reconnect to the projector */ reconnectProjector() { try { this.log.info(`PJLink trying to (re)connect to projector`); // only the getPowerState for now // @ts-ignore this.projector.getPowerState(this.pjlinkAnswerHandler.bind(this, 'GETPOWERSTATE')); // and set the reconnect delay in advance, but only if not running if (!this.timers.reconnectDelay) this.timers.reconnectDelay = setInterval(this.reconnectProjector.bind(this), this.config.reconnectDelay); } catch (err) { this.errorHandler(err, 'reconnectProjector'); } } /** * check which status query has to be done * @param {string} interval 'startup', 'short' or 'long' */ doStatusQuery(interval) { try { this.log.debug(`PJLink requesting projector information for interval: '${interval}'`); if (interval === 'startup') { // only called on projector connected if (!this.firstRunDone) { this.firstRunDone = true; this.doQuery(this.statusQueryInfo.startup); } } if (interval === 'short') { // at first skip the cycles after power ON if (this.skippedShortCycles > 0) { this.skippedShortCycles--; this.log.debug(`PJLink skipping 'short' cycle no: ${this.skippedShortCycles}`); return; } this.skippedShortCycles = -1; // set to expired // try to do the startupPowered queries if (!this.firstRunPowered && this.poweredOn) { this.firstRunPowered = true; this.doQuery(this.statusQueryInfo.startupPowered); } this.doQuery(this.statusQueryInfo.short); if (this.poweredOn) this.doQuery(this.statusQueryInfo.shortPowered); } if (interval === 'long') { this.doQuery(this.statusQueryInfo.long); if (this.poweredOn) this.doQuery(this.statusQueryInfo.longPowered); } } catch (err) { this.errorHandler(err, 'doStatusQuery'); } } /** * check which status query has to be done * @param {Object} queriesTodo a array with the queries which has to be done */ doQuery(queriesTodo) { try { for (const code of queriesTodo) { // ['POWR', 'INPT', 'CLSS', 'AVMT', 'ERST', 'LAMP', 'INST', 'NAME', 'INF1', 'INF2', 'INFO'] switch (code) { case 'POWR': // @ts-ignore this.projector.getPowerState(this.pjlinkAnswerHandler.bind(this, 'GETPOWERSTATE')); break; case 'INPT': // @ts-ignore this.projector.getInput(this.pjlinkAnswerHandler.bind(this, 'GETINPUT')); break; case 'CLSS': // @ts-ignore this.projector.getClass(this.pjlinkAnswerHandler.bind(this, 'GETCLASS')); break; case 'AVMT': // @ts-ignore this.projector.getMute(this.pjlinkAnswerHandler.bind(this, 'GETMUTE')); break; case 'ERST': // @ts-ignore this.projector.getErrors(this.pjlinkAnswerHandler.bind(this, 'GETERRORS')); break; case 'LAMP': // @ts-ignore this.projector.getLamps(this.pjlinkAnswerHandler.bind(this, 'GETLAMPS')); break; case 'INST': // @ts-ignore this.projector.getInputs(this.pjlinkAnswerHandler.bind(this, 'GETINPUTS')); break; case 'NAME': // @ts-ignore this.projector.getName(this.pjlinkAnswerHandler.bind(this, 'GETNAME')); break; case 'INF1': // @ts-ignore this.projector.getManufacturer(this.pjlinkAnswerHandler.bind(this, 'GETMANUFACTURER')); break; case 'INF2': // @ts-ignore this.projector.getModel(this.pjlinkAnswerHandler.bind(this, 'GETMODEL')); break; case 'INFO': // @ts-ignore this.projector.getInfo(this.pjlinkAnswerHandler.bind(this, 'GETINFO')); break; default: this.errorHandler(`Unknown query code '${code}'`, 'doStatusQuery'); } } } catch (err) { this.errorHandler(err, 'doStatusQuery'); } } /** * Called to refresh the projector status, main timer routine */ getProjectorStatus() { try { this.doStatusQuery('short'); // check wheter to do the long interval this.actStatusQryForInfo++; if (this.actStatusQryForInfo >= this.numStatusQryForInfo) { this.actStatusQryForInfo = 0; this.doStatusQuery('long'); } } catch (err) { this.errorHandler(err, 'getProjectorStatus'); } } /** * Called to turn the projector on or off depemding on its actual state */ async projectorOnOff() { try { this.log.info(`PJLink power button pressed`); // first get the projector status const state = await this.getStateAsync('powerStatus'); // @ts-ignore const powerStatus = state.val || 0; // reset power button status. Set as confirmed by hardware (ack = true) this.setState('power', false, true); if (powerStatus === 0) { this.log.info(`PJLink Projector is currently off. Trying to switch projector on`); // @ts-ignore this.projector.powerOn(); this.skippedShortCycles = this.config.skippedCyclesAfterPowerOn; this.log.debug(`PJLink now skipping ${this.skippedShortCycles} times the 'short' query cycle`); return; } if (powerStatus === 1) { this.log.info(`PJLink Projector is currently on. Trying to switch projector off`); // @ts-ignore this.projector.powerOff(); return; } // return if cooling or warming up if (powerStatus === 2) { this.log.info(`PJLink Projector is currently cooling down. Refuse to switch power`); return; } if (powerStatus === 3) { this.log.info(`PJLink Projector is currently warming up. Refuse to switch power`); return; } } catch (err) { this.errorHandler(err, 'projectorOnOff'); } } /** * Called to set the mute status * @param {number} status */ async setMute(status) { try { this.log.info(`PJLink mute status changed to: ${status}`); // @ts-ignore this.projector.setMute(status, this.pjlinkAnswerHandler.bind(this, 'ERROR')); } catch (err) { this.errorHandler(err, 'setMute'); } } /** * Called as answer function from pjlink functions * @param {string} command called commands from PJLink to separate the value handling * @param {any} pjlinkValues normaly the err and the state from the PJLink function call */ async pjlinkAnswerHandler(command, ...pjlinkValues) { try { // first look at the error state const error = pjlinkValues[0]; let state = ''; if (error) { switch (error.message) { case 'Unavailable time': if (!this.unavailableTime) { this.unavailableTime = true; this.log.warn(`pjlinkAnswerHandler (command: ${command}), Projector is actualy unavailable. This is only logged once`); } break; case 'Undefined command': case 'Out of parameter': case 'Authorization failed': case 'Command reply mismatch': case 'Not connected': this.unavailableTime = false; this.log.error(`pjlinkAnswerHandler (command: ${command}), Projector send error: ${error.message}`); break; case 'Projector/Display failure': default: this.unavailableTime = true; this.errorHandler(error, `pjlinkAnswerHandler (command: ${command})`); // reset connection state this.setState('info.connection', false, true); // stop/restart timers clearInterval(this.timers.statusDelay); if (!this.timers.reconnectDelay) { // Start reconnection only once this.timers.reconnectDelay = setInterval(this.reconnectProjector.bind(this), this.config.reconnectDelay); } } return; } if (pjlinkValues.length > 1) { state = pjlinkValues[1]; this.log.debug(`PJLink got answer from command: '${command}', value '${JSON.stringify(state)}'`); // reset unavailable time this.unavailableTime = false; // only if the reconnect timer is not cleared, this means, that the connection has been freshley established if (this.timers.reconnectDelay) { this.log.info(`PJLink established connection to the projector`); // get the status info on startup this.doStatusQuery('startup'); // clear the reconnect mechanism this.clearInterval(this.timers.reconnectDelay); this.timers.reconnectDelay = undefined; // start timer for status and information update this.timers.statusDelay = setInterval(this.getProjectorStatus.bind(this), this.config.statusDelay); // set connection state this.setState('info.connection', true, true); } // now parse the return values let fan = 0; let lamp = 0; let temperature = 0; let cover = 0; let filter = 0; let other = 0; switch (command) { case 'ERROR': // only for error handling in callback. break; case 'GETPOWERSTATE': this.setState('powerStatus', parseInt(state), true); if (state == '1') { this.poweredOn = true; if (this.skippedShortCycles != -1) this.skippedShortCycles = this.config.skippedCyclesAfterPowerOn; } else { this.poweredOn = false; this.skippedShortCycles = 0; // reset expired } break; case 'GETINPUT': // @ts-ignore this.setState('input', parseInt(state.code), true); break; case 'GETMUTE': // @ts-ignore this.setState('videoMuteStatus', state.video, true); // @ts-ignore this.setState('audioMuteStatus', state.audio, true); // @ts-ignore this.setState('setMute', state.status, true); // new extended mute status break; case 'GETERRORS': if (state) { // @ts-ignore fan = state.fan === 'warning' ? 1 : state.fan === 'error' ? 3 : 0; // @ts-ignore lamp = state.lamp === 'warning' ? 1 : state.lamp === 'error' ? 3 : 0; // @ts-ignore temperature = state.temperature === 'warning' ? 1 : state.temperature === 'error' ? 3 : 0; // @ts-ignore cover = state.cover === 'warning' ? 1 : state.cover === 'error' ? 3 : 0; // @ts-ignore filter = state.filter === 'warning' ? 1 : state.filter === 'error' ? 3 : 0; // @ts-ignore other = state.other === 'warning' ? 1 : state.other === 'error' ? 3 : 0; } this.setState('deviceInfo.fanErrorStatus', fan, true); this.setState('deviceInfo.lampErrorStatus', lamp, true); this.setState('deviceInfo.temperatureErrorStatus', temperature, true); this.setState('deviceInfo.coverOpenStatus', cover, true); this.setState('deviceInfo.filterErrorStatus', filter, true); this.setState('deviceInfo.otherErrorStatus', other, true); break; case 'GETLAMPS': // @ts-ignore this.setState('deviceInfo.lamps.lamp1Status', parseInt(state[0].on === false ? 0 : 1), true); // @ts-ignore this.setState('deviceInfo.lamps.lamp1Hours', parseInt(state[0].hours), true); for (let lamps = 1; lamps < state.length; lamps++) { const index = lamps + 1; await this.setObjectNotExistsAsync(`deviceInfo.lamps.lamp${index}Status`, { 'type': 'state', 'common': { 'role': 'indicator.maintenance', 'name': { 'en': 'Status of lamp ' + index, 'de': 'Status der Lampe ' + index, 'ru': 'Статус лампы ' + index, 'pt': 'Estado da lâmpada ' + index, 'nl': 'Status van lamp ' + index, 'fr': 'État du feu ' + index, 'it': 'Stato della lampada ' + index, 'es': 'Estado de la lámpara ' + index, 'pl': 'Status lampy ' + index, 'uk': 'Статус лампи ' + index, 'zh-cn': '口粮' + index }, 'type': 'number', 'states': { '0': 'Off', '1': 'On' }, 'read': true, 'write': false, 'def': 0 }, 'native': {} }); await this.setObjectNotExistsAsync(`deviceInfo.lamps.lamp${index}Hours`, { '_id': 'deviceInfo.lamps.lamp1Hours', 'type': 'state', 'common': { 'role': 'value', 'name': { 'en': 'Lighting time of lamp ' + index, 'de': 'Leuchtdauer der Lampe ' + index, 'ru': 'Время освещения лампы ' + index, 'pt': 'Tempo de iluminação da lâmpada ' + index, 'nl': 'Verlichtingstijd van lamp ' + index, 'fr': 'Temps d\'éclairage de la lampe ' + index, 'it': 'Tempo di illuminazione della lampada ' + index, 'es': 'Tiempo de iluminación de la lámpara ' + index, 'pl': 'Czas świetlny lampy ' + index, 'uk': 'Час освітлення лампи ' + index, 'zh-cn': 'A. 灯' + index }, 'type': 'number', 'min': 0, 'max': 99999, 'read': true, 'write': false, 'def': 0 }, 'native': {} }); // @ts-ignore this.setState(`deviceInfo.lamps.lamp${index}Status`, parseInt(state[lamps].on === false ? 0 : 1), true); // @ts-ignore this.setState(`deviceInfo.lamps.lamp${index}Hours`, parseInt(state[lamps].hours), true); } break; case 'GETINPUTS': this.setState('deviceInfo.inputsAvailable', JSON.stringify(state), true); break; case 'GETNAME': this.setState('deviceInfo.projectorName', JSON.stringify(state), true); break; case 'GETMANUFACTURER': this.setState('deviceInfo.projectorManufacturer', JSON.stringify(state), true); break; case 'GETMODEL': this.setState('deviceInfo.productName', JSON.stringify(state), true); break; case 'GETINFO': this.setState('deviceInfo.otherInfo', JSON.stringify(state), true); break; case 'GETCLASS': this.setState('deviceInfo.class', parseInt(state), true); break; default: this.log.info(`PJLink unsupported command '${command}'`); } } } catch (err) { this.errorHandler(err, 'pjlinkAnswerHandler'); } } /** * Called on error situations and from catch blocks * @param {any} err * @param {string} module */ errorHandler(err, module = '') { this.log.error(`PJLink error in method: [${module}] error: ${err.message}, stack: ${err.stack}`); } /** * Is called when adapter shuts down - callback has to be called under any circumstances! * @param {() => void} callback */ onUnload(callback) { try { // End the PJLink connection // @ts-ignore this.projector.disconnect(); // Here you must clear all timeouts or intervals that may still be active clearInterval(this.timers.statusDelay); clearInterval(this.timers.reconnectDelay); // Reset the connection indicator this.setState('info.connection', false, true); callback(); } catch (e) { callback(); } } /** * Is called if a subscribed state changes * @param {string} id * @param {ioBroker.State | null | undefined} state */ onStateChange(id, state) { try { if (state) { if (state.ack) { this.log.debug(`PJLink state ${id} changed: ${state.val} (ack = ${state.ack})`); } else { this.log.info(`PJLink state ${id} changed: ${state.val} (ack = ${state.ack})`); } // The state was changed if (!state.ack) { // only if the state is set manually const onlyId = id.replace(this.namespace + '.', ''); switch (onlyId) { case 'power': this.projectorOnOff(); break; case 'input': // the string value is parsed by the pjlink.inputCommand. // For the future and Class 2 it is the preferred format because of e.g. input 3B // @ts-ignore this.projector.setInput(state.val.toString()); break; case 'setMute': // @ts-ignore this.setMute(parseInt(state.val)); break; } } } } catch (err) { this.errorHandler(err, 'onStateChange'); } } /** * Some message was sent to this instance over message box. Used by email, pushover, text2speech, ... * Using this method requires "common.messagebox" property to be set to true in io-package.json * @param {ioBroker.Message} obj */ async onMessage(obj) { if (typeof obj === 'object') { if (obj.command === 'updateInputs') { this.log.debug(`updateInputs command gets: ${JSON.stringify(obj)}`); const state = await this.getStateAsync('deviceInfo.inputsAvailable'); // @ts-ignore const inputsAvailable = state.val || ''; this.log.info(`Set the following inputs in the native object "system.adapter.${this.namespace}": ${JSON.stringify(inputsAvailable)}`); const instanceObject = await this.getForeignObjectAsync(`system.adapter.${this.namespace}`) || {}; try { // @ts-ignore instanceObject.native.inputInfo = JSON.parse(inputsAvailable); } catch (err) { this.errorHandler(err, 'onMessage (parse inputsAvailable)'); } // @ts-ignore await this.setForeignObjectAsync(`system.adapter.${this.namespace}`, instanceObject); if (obj.callback) { this.sendTo(obj.from, obj.command, 'done', obj.callback); } } if (obj.command === 'setInstanceInputs') { if (obj.command === 'setInstanceInputs') { this.log.debug(`setInstanceInputs command gets: ${JSON.stringify(obj)}`); const inputObj = await this.getObjectAsync('input'); // @ts-ignore if (inputObj.common.min) delete (inputObj.common.min); // @ts-ignore if (inputObj.common.max) delete (inputObj.common.max); // @ts-ignore inputObj.common.states = {}; for (const i in this.config.inputInfo) { // @ts-ignore const inputCode = this.config.inputInfo[i].code; // @ts-ignore const inputName = this.config.inputInfo[i].name; // @ts-ignore Object.assign(inputObj.common.states, {[inputCode] : inputName}); } // @ts-ignore this.log.info(`setInstanceInputs command sets inputs common to: ${JSON.stringify(inputObj.common)}`); // @ts-ignore await this.setObjectAsync('input', inputObj); if (obj.callback) { this.sendTo(obj.from, obj.command, 'done', obj.callback); } } } if (obj.command === 'resetInstanceInputs') { if (obj.command === 'resetInstanceInputs') { this.log.debug(`resetInstanceInputs command gets: ${JSON.stringify(obj)}`); const inputObj = await this.getObjectAsync('input'); // @ts-ignore if (inputObj.common.states) delete (inputObj.common.states); // @ts-ignore inputObj.common.min = 11; // @ts-ignore inputObj.common.max = 59; // @ts-ignore this.log.info(`resetInstanceInputs command sets inputs common to: ${JSON.stringify(inputObj.common)}`); // @ts-ignore await this.setObjectAsync('input', inputObj); if (obj.callback) { this.sendTo(obj.from, obj.command, 'done', obj.callback); } } } } } } if (require.main !== module) { // Export the constructor in compact mode /** * @param {Partial<utils.AdapterOptions>} [options={}] */ module.exports = (options) => {'use strict'; new Pjlink(options); }; } else { // otherwise start the instance directly new Pjlink(); }