From a66a2f8c47a92f6f67f389636e03dbdbd8629046 Mon Sep 17 00:00:00 2001 From: Bannsaenger Date: Mon, 23 Jan 2023 21:06:08 +0000 Subject: [PATCH] temporarily fix testing --- README.md | 4 + lib/command.js | 402 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/pjlink.js | 295 +++++++++++++++++++++++++++++++++++ lib/response.js | 86 +++++++++++ main.js | 26 +++- package.json | 3 +- 6 files changed, 813 insertions(+), 3 deletions(-) create mode 100644 lib/command.js create mode 100644 lib/pjlink.js create mode 100644 lib/response.js diff --git a/README.md b/README.md index 890e81f..6808d51 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ This work is based on the nodejs module with pjlink implementation from **sy1vai ## ToDo * support the node-pjlink project to implement class 2 +* go back to the pjlink library on github. For now the library is held localy because of an error in the rest script ## How the adapter works For now only class 1 is supported. This means the adapter can only poll the status. @@ -82,6 +83,9 @@ the other lamps will be added dynamically. Placeholder for the next version (at the beginning of the line): ### **WORK IN PROGRESS** --> +### **WORK IN PROGRESS** +* (Bannsaenger) temporarily fix the test script error with local libraries + ### 0.1.0 (2023-01-23) * (Bannsaenger) extended configuration to let you choose the frequency and time for information retrieval * (Bannsaenger) add possibility to customize media.input by the **INST** query and edit the names in instance config diff --git a/lib/command.js b/lib/command.js new file mode 100644 index 0000000..6de5f60 --- /dev/null +++ b/lib/command.js @@ -0,0 +1,402 @@ +var util = require('util'), + extend = require('extend'), + response= require('./response'); + +var Command = function(cmd, args, cb){ + this.cmd = cmd || "INVA"; + if(typeof args!=='object' && typeof args!=='undefined' && args!==null){ + args = [args]; + } + this.args = args; + this.cb = cb; +} + +Command.prototype.toString = function(){ + var str = this.cmd; + + if(this.args){ + for(var i=0; i0){ + data = args.shift(); + } + + Command.call(this, 'POWR', data, cb); +} + +util.inherits(Command.PowerCommand, Command); + +Command.PowerCommand.prototype.formatResult = function(args){ + if(args.length>0){ + return parseInt(args[0]); + } +} + + +/************* INPUT ************/ +Command.INPUT = { + RGB: 1, VIDEO: 2, DIGITAL: 3, STORAGE: 4, NETWORK: 5 +} + +Command.InputCommand = function(){ + var cb; + var data = '?'; + var args = Array.prototype.slice.call(arguments, 0); + + if(typeof args[args.length-1]==='function') cb = args.pop(); + + if(args.length>0){ + var input = {source: 'rgb', channel: 1} + + if(typeof args[0]=='object'){ + input = extend(input, args[0]); + }else if(typeof args[0]=='string' && args[0].length==2){ //string code + input.source = parseInt(args[0].substr(0,1)); + input.channel = parseInt(args[0].substr(1,1)); + }else if(typeof args[0]=='number' && args[0]>=10){ //number code + input.channel = args[0]%10; + input.source = (args[0]-input.channel)/10; + }else{ + input.source = args.shift(); + if(args.length) input.channel = args.shift(); + } + + if(typeof input.source==='string'){ + input.source = Command.INPUT[input.source.toUpperCase()]; + } + + data = input.source + '' + input.channel; + + } + + Command.call(this, 'INPT', data, cb); +} +util.inherits(Command.InputCommand, Command); + +Command.InputCommand.prototype.formatResult = function(args){ + if(args.length>0){ + args = args[0]; + if(typeof args==='string' && args.length>1){ + try{ + var source = parseInt(args.substr(0,1)); + var channel = parseInt(args.substr(1,1)); + return {source: source, channel: channel, code: args, name: findSourceName(source, channel)}; + }catch(e){} + } + } +} + +var findSourceName = function(source, channel){ + for(var key in Command.INPUT){ + if(Command.INPUT[key]==source){ + return key + ' - ' + channel; + } + } + + return 'unknown'; +} + +/************* MUTE ************/ + +Command.MuteCommand = function(){ + var cb; + var data = '?'; + var args = Array.prototype.slice.call(arguments, 0); + + if(typeof args[args.length-1]==='function') cb = args.pop(); + + if(args.length>0){ + var mute = {audio: false, video: false}; + + if(typeof args[0]=='number') { + // handling of extendet mute status + data = args[0]; + } else { + if (typeof args[0]=='object'){ + mute = extend(mute, args[0]); + } else { + mute.video = args.shift(); + if(args.length) mute.audio = args.shift(); + } + + //if both the same + if (mute.audio==mute.video){ + if (mute.audio){ + data = '31'; + } else { + data = '30'; + } + } else { + if (mute.video){ + data = '11'; + } else { + data = '21'; + } + } + } + } + + Command.call(this, 'AVMT', data, cb); +} + +util.inherits(Command.MuteCommand, Command); + +Command.MuteCommand.prototype.formatResult = function(args){ + // Added extended mute status + var mute = {audio: false, video: false, status: 30} + if(args.length>0){ + args = args[0]; + if(typeof args==='string' && args.length>1){ + + var c = args.substr(0,1); + var v = (args.substr(1,1)=='1'); + + mute.status = parseInt(args); + + switch(c){ + case '1': + mute.video = v; + break; + case '2': + mute.audio = v; + break; + case '3': + mute.video = mute.audio = v; + break; + } + } + } + return mute; +} + +/************* ERRORS ************/ + +Command.ErrorsCommand = function(){ + var cb; + var data = '?'; + var args = Array.prototype.slice.call(arguments, 0); + + if(typeof args[args.length-1]==='function') cb = args.pop(); + + Command.call(this, 'ERST', data, cb); +} + +util.inherits(Command.ErrorsCommand, Command); + +Command.ErrorsCommand.prototype.formatResult = function(args){ + if(args.length>0){ + args = args[0]; + if(typeof args==='string' && args.length==6){ + if(parseInt(args)!=0){ + var err = { + fan: args.substr(0,1), + lamp: args.substr(1,1), + temperature: args.substr(2,1), + cover: args.substr(3,1), + filter: args.substr(4,1), + other: args.substr(5,1) + } + for(k in err){ + if(err.hasOwnProperty(k)){ + err[k] = (err[k]=='2'?'error':(err[k]=='1')?'warning':false); + } + } + return err; + } + } + } + return null; +} + +/************* LAMP ************/ + +Command.LampCommand = function(){ + var cb; + var data = '?'; + var args = Array.prototype.slice.call(arguments, 0); + + if(typeof args[args.length-1]==='function') cb = args.pop(); + + Command.call(this, 'LAMP', data, cb); +} + +util.inherits(Command.LampCommand, Command); + +Command.LampCommand.prototype.formatResult = function(args){ + var lamps = []; + + if(args.length>0 && args.length%2==0){ + for(var i=0; i1){ + try{ + var source = parseInt(input.substr(0,1)); + var channel = parseInt(input.substr(1,1)); + inputs.push( + {source: source, channel: channel, code: input, name: findSourceName(source, channel)} + ); + }catch(e){} + } + } + + return [inputs]; +} + +/************* Name ************/ + +Command.NameCommand = function(){ + var cb; + var data = '?'; + var args = Array.prototype.slice.call(arguments, 0); + + if(typeof args[args.length-1]==='function') cb = args.pop(); + + Command.call(this, 'NAME', data, cb); +} + +util.inherits(Command.NameCommand, Command); + +/************* MANUFACTURER ************/ + +Command.ManufacturerCommand = function(){ + var cb; + var data = '?'; + var args = Array.prototype.slice.call(arguments, 0); + + if(typeof args[args.length-1]==='function') cb = args.pop(); + + Command.call(this, 'INF1', data, cb); +} + +util.inherits(Command.ManufacturerCommand, Command); + +/************* MODEL ************/ + +Command.ModelCommand = function(){ + var cb; + var data = '?'; + var args = Array.prototype.slice.call(arguments, 0); + + if(typeof args[args.length-1]==='function') cb = args.pop(); + + Command.call(this, 'INF2', data, cb); +} + +util.inherits(Command.ModelCommand, Command); + +Command.ModelCommand.prototype.formatResult = function(args) { + return args.join(" "); +} + +/************* INFO ************/ + +Command.InfoCommand = function(){ + var cb; + var data = '?'; + var args = Array.prototype.slice.call(arguments, 0); + + if(typeof args[args.length-1]==='function') cb = args.pop(); + + Command.call(this, 'INFO', data, cb); +} + +util.inherits(Command.InfoCommand, Command); + +/************* CLASS ************/ + +Command.ClassCommand = function(){ + var cb; + var data = '?'; + var args = Array.prototype.slice.call(arguments, 0); + + if(typeof args[args.length-1]==='function') cb = args.pop(); + + Command.call(this, 'CLSS', data, cb); +} + +util.inherits(Command.ClassCommand, Command); + + +module.exports = Command; diff --git a/lib/pjlink.js b/lib/pjlink.js new file mode 100644 index 0000000..45de283 --- /dev/null +++ b/lib/pjlink.js @@ -0,0 +1,295 @@ +var util = require('util'), + net = require('net'), + crypto = require('crypto'), + pjcommand = require('./command'), + pjresponse = require('./response'); + +var PJLink = function(host, port, password){ + + var settings = {}; + if(typeof host==="object"){ + settings = host; + }else{ + if(host) settings.host = host; + if(port) settings.port = port; + if(password) settings.password = password; + } + + this.settings = { + "host": "192.168.1.1", + "port": 4352, + "password": null, + "timeout": 0 + }; + + this.class = 1; + + this._connection = null; + this._digest = null; + this._cmdQueue = []; + this._cmdWaiting = false; + + var mergedSettings = {}; + for (var attrname in this.settings) { mergedSettings[attrname] = this.settings[attrname]; } + for (var attrname in settings) { mergedSettings[attrname] = settings[attrname]; } + + this.settings = mergedSettings; +}; + +PJLink.prototype.disconnect = function(){ + this._resetConnection(); +} + +/************/ +/* POWER */ +/************/ +PJLink.POWER = pjcommand.POWER; + +PJLink.prototype.powerOn = function(cb){ + this.setPowerState(PJLink.POWER.ON, cb); +} + +PJLink.prototype.powerOff = function(cb){ + this.setPowerState(PJLink.POWER.OFF, cb); +} + +PJLink.prototype.setPowerState = function(state, cb){ + this._addCommand(new pjcommand.PowerCommand(state, cb)); +} + +PJLink.prototype.getPowerState = function(cb){ + this._addCommand(new pjcommand.PowerCommand(cb)); +} + +/************/ +/* INPUT */ +/************/ +PJLink.INPUT = pjcommand.INPUT; + +PJLink.prototype.setInput = function(){ + var args = Array.prototype.slice.call(arguments, 0); + args.unshift(null); + this._addCommand( + new (pjcommand.InputCommand.bind.apply(pjcommand.InputCommand, args))() + ); +} + +PJLink.prototype.getInput = function(cb){ + this._addCommand(new pjcommand.InputCommand(cb)); +} + +/************/ +/* MUTE */ +/************/ +PJLink.prototype.setMute = function(val, cb){ + this._addCommand( + new pjcommand.MuteCommand(val, cb) + ); +} + +PJLink.prototype.getMute = function(cb){ + this._addCommand( + new pjcommand.MuteCommand(cb) + ); +} + +/************/ +/* ERROR */ +/************/ +PJLink.prototype.getErrors = function(cb){ + this._addCommand( + new pjcommand.ErrorsCommand(cb) + ); +} + +/************/ +/* LAMP */ +/************/ +PJLink.prototype.getLamps = function(cb){ + this._addCommand( + new pjcommand.LampCommand(cb) + ); +} + +/************/ +/* LAMP */ +/************/ +PJLink.prototype.getInputs = function(cb){ + this._addCommand( + new pjcommand.InputsCommand(cb) + ); +} + +/************/ +/* NAME */ +/************/ +PJLink.prototype.getName = function(cb){ + this._addCommand( + new pjcommand.NameCommand(cb) + ); +} + +/************/ +/* MANUFACTURER */ +/************/ +PJLink.prototype.getManufacturer = function(cb){ + this._addCommand( + new pjcommand.ManufacturerCommand(cb) + ); +} + +/************/ +/* MODEL */ +/************/ +PJLink.prototype.getModel = function(cb){ + this._addCommand( + new pjcommand.ModelCommand(cb) + ); +} + +/************/ +/* INFO */ +/************/ +PJLink.prototype.getInfo = function(cb){ + this._addCommand( + new pjcommand.InfoCommand(cb) + ); +} + +/************/ +/* CLASS */ +/************/ +PJLink.prototype.getClass = function(cb){ + this._addCommand( + new pjcommand.ClassCommand(cb) + ); +} + + + +//** PRIVATE FUNCTIONS **/ +PJLink.prototype._connect = function(){ + this._cmdWaiting = true; + this._connection = net.connect({port: this.settings.port, host: this.settings.host}, this._onConnect.bind(this)); + + //callbacks + this._dataCB = this._onData.bind(this); + this._errorCB = this._onError.bind(this); + this._closeCB = this._onClose.bind(this); + this._timeoutCB = this._onTimeout.bind(this); + + this._connection.on('data', this._dataCB); + this._connection.on('error', this._errorCB); + this._connection.on('close', this._closeCB); + this._connection.on('timeout', this._timeoutCB); + this._connection.setTimeout(this.settings.timeout); + this._connection.setNoDelay(true); + +} + +PJLink.prototype._onConnect = function(){ + this._cmdWaiting = false; +} + +PJLink.prototype._onData = function(buffer){ + var response = pjresponse.parse(buffer); + + if(!response) return; //what went wrong? + + //it is a non-error auth command + if(response.cmd==pjresponse.AUTH && !response.isError()){ + if(response.hasArgs()){ + this._calcDigest(response.args[0]); + } + }else{ + //process the first command in the queue + this._handleResponse(response); + } + + //do the next one on next occasion + process.nextTick(this._sendCommand.bind(this)); + +} + +PJLink.prototype._onError = function(err){ + this._handleResponse(new pjresponse(null, err.message)); +} + +PJLink.prototype._onClose = function(had_error){ + this._resetConnection(); +} + +PJLink.prototype._resetConnection = function(){ + if(this._cmdWaiting){ + this._handleResponse(new pjresponse(null, pjresponse.getError(pjresponse.ERRORS.ERRD))); + } + + if(this._connection){ + this._connection.removeListener('data', this._dataCB); + this._connection.removeListener('error', this._errorCB); + this._connection.removeListener('close', this._closeCB); + this._connection.removeListener('timeout', this._timeoutCB); + if (this._connection.connecting) { // If timeout occurs while connecting e.g. host not available then destroy + this._connection.destroy(); // because end will not work here + } else { + this._connection.end(); + } + } + + //reset the connection etc + this._connection = null; + this._digest = null; + process.nextTick(this._sendCommand.bind(this)); +} + +PJLink.prototype._onTimeout = function(){ + var error = new Error('Connection timeout'); + while(this._cmdQueue.length){ + this._onError(error); + } + this._onClose(); +} + +PJLink.prototype._calcDigest = function(rand){ + var md5 = crypto.createHash('md5'); + md5.setEncoding('hex'); + md5.write(rand); + md5.end(this.settings.password); + this._digest = md5.read(); +} + +PJLink.prototype._addCommand = function(cmd){ + this._cmdQueue.push(cmd); + + if(!this._cmdWaiting){ + this._sendCommand(); + } +} + +PJLink.prototype._sendCommand = function(){ + if(this._cmdQueue.length==0) return; + if(!this._connection) return this._connect(); + if(this._cmdWaiting) return; + + this._cmdWaiting = true; + + var msg = '%' + this.class + this._cmdQueue[0].toString(); + + if(this._digest){ + msg = this._digest + msg; + this._digest = null; //always reset after first send + } + + this._connection.write(msg + "\r", function(){}); +} + +PJLink.prototype._handleResponse = function(response){ + if(!response || this._cmdQueue.length==0) return; + + var cmd = this._cmdQueue.shift(); + if(cmd) cmd.handleResponse(response); + + this._cmdWaiting = false; +} + +module.exports = PJLink; diff --git a/lib/response.js b/lib/response.js new file mode 100644 index 0000000..efa007c --- /dev/null +++ b/lib/response.js @@ -0,0 +1,86 @@ +var Response = function(cmd, err, args){ + this.cmd = cmd; + this.err = err; + this.args = args; + + if(args && typeof this.args!=='object'){ + this.args = [this.args]; + } +} + +Response.AUTH = 'PJLINK'; + +Response.ERRORS = { + 'OK': null, + 'ERR1': 'Undefined command', + 'ERR2': 'Out of parameter', + 'ERR3': 'Unavailable time', + 'ERR4': 'Projector/Display failure', + 'ERRA': 'Authorization failed', + 'ERRM': 'Command reply mismatch', + 'ERRD': 'Not connected' +} + +Response.prototype.isError = function(){ + return this.err!==null && typeof this.err!=='undefined'; +} + +Response.prototype.getError = function(){ + if(!this.isError()) return null; + + if(typeof this.err == 'object') return this.err; + + return Response.getError(this.err); +} + +Response.getError = function(err){ + if(!err) return null; + return new Error(err); +} + +Response.prototype.hasArgs = function(){ + return this.args && this.args.length>0; +} + +Response.prototype.getArgs = function(){ + return this.args; +} + +Response.parse = function(data){ + if(typeof data==='object' && data.toString) data = data.toString(); + + data = data.trim();//cut off trailing CR + + var cmd, args, err; + + var cmd = data.substr(0,6).toUpperCase();//header + command; + data = data.substr(7);//skip to the data + + //the data should now contain an error if it is there + if(Response.ERRORS.hasOwnProperty(data)){ + err = Response.ERRORS[data]; + data = null; + } + + //handle authentication + if(cmd==Response.AUTH){ + //more general error checking here + if(!err){ + data = data.substr(2);//we read over the 0 or 1 + } + }else{ + cmd = cmd.substr(2);//read the space character + } + + if(data && data.length){ + args = data.split(' '); + } + + return new Response(cmd,err,args); +} + +Response.prototype.cmd = null; +Response.prototype.args = null; +Response.prototype.cls = 1; + +module.exports = Response; diff --git a/main.js b/main.js index 93e07be..3d59dfa 100644 --- a/main.js +++ b/main.js @@ -11,7 +11,7 @@ */ const utils = require('@iobroker/adapter-core'); -const pjlink = require('pjlink'); +const pjlink = require('./lib/pjlink.js'); // possible query types const queries = ['POWR', 'INPT', 'CLSS', 'AVMT', 'ERST', 'LAMP', 'INST', 'NAME', 'INF1', 'INF2', 'INFO']; @@ -24,6 +24,14 @@ const queries = ['POWR', 'INPT', 'CLSS', 'AVMT', 'ERST', 'LAMP', 'INST', 'NAME', * 3 / pjlink.POWER.WARMING_UP */ +/* +ToDo: go back to pjlink library from github. +package.json: +, + "pjlink": "Bannsaenger/node-pjlink" + + +*/ class Pjlink extends utils.Adapter { @@ -158,6 +166,7 @@ class Pjlink extends utils.Adapter { 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); @@ -214,46 +223,57 @@ class Pjlink extends utils.Adapter { // ['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; @@ -299,6 +319,7 @@ class Pjlink extends utils.Adapter { 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`); @@ -306,6 +327,7 @@ class Pjlink extends utils.Adapter { } if (powerStatus === 1) { this.log.info(`PJLink Projector is currently on. Trying to switch projector off`); + // @ts-ignore this.projector.powerOff(); return; } @@ -330,6 +352,7 @@ class Pjlink extends utils.Adapter { 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) { @@ -587,6 +610,7 @@ class Pjlink extends utils.Adapter { 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 diff --git a/package.json b/package.json index 2c3e73d..142d75c 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,7 @@ "url": "https://github.com/Bannsaenger/ioBroker.pjlink" }, "dependencies": { - "@iobroker/adapter-core": "^2.6.7", - "pjlink": "Bannsaenger/node-pjlink" + "@iobroker/adapter-core": "^2.6.7" }, "devDependencies": { "@alcalzone/release-script": "^3.5.9",