pladaria / reconnecting-websocket

Reconnecting WebSocket. For Web, React Native, cli (Node.js)
MIT License
1.22k stars 197 forks source link

Error: No valid WebSocket class provided #135

Open mastin-zgz opened 4 years ago

mastin-zgz commented 4 years ago

Good morning,

I am trying to add the reconnect websocket to my code, but the debug of the red node gives me the error "Error: No valid WebSocket class provided"

my code is this:

`const WS = require('ws'); const WebSocket = require('ws'); const uuidv4 = require('uuid/v4'); const events = require('events'); const EventEmitter = events.EventEmitter; const Logger = require('./utils/logdata'); const debug = require('debug')('anl:ocpp:cp:server:json'); const ReconnectingWebSocket = require('reconnecting-websocket'); let ee = new EventEmitter(); module.exports = function(RED) { 'use strict'; function OCPPChargePointJNode(config) { RED.nodes.createNode(this, config); debug('Starting CP client JSON node'); const CALL = 2; const CALLRESULT = 3;

// for logging...
const msgTypeStr = ['unknown', 'unknown', 'received', 'replied', 'error'];

const msgType = 0;
const msgId = 1;
const msgAction = 2;
const msgCallPayload = 3;
const msgResPayload = 2;
var obj = require('./global.json'); 
var node = this;

node.reqKV = {};

this.remotecs = RED.nodes.getNode(config.remotecs);
this.url = obj.url;
this.cbId = obj.cbId;
this.ocppVer = this.ocppver;
this.name = obj.nombre || config.name || this.remotecs.name;
this.command = config.command;
this.cmddata = config.cmddata;
this.logging = config.log || false;
this.pathlog = config.pathlog;

const options = {
    WebSocket: WS, // custom WebSocket constructor
    connectionTimeout: 1000,
    maxRetries: 10,
};

const logger = new Logger(this, this.pathlog, this.name);
logger.enabled = (this.logging && (typeof this.pathlog === 'string') && this.pathlog !== '');

let csUrl = `${this.url}/${this.cbId}`;

logger.log('info', `Realizando conexion websocket a ${csUrl}`);

let rws = new ReconnectingWebSocket(csUrl, options);

ws.on('open', function(){
  logger.log('info', `Connectado a ${csUrl}`);
  node.log(`Connectado a ${csUrl}`);
  debug(`Connectado a ${csUrl}`);
  node.status({fill: 'green', shape: 'dot', text: 'Conectado...'});
  node.wsconnected = true;
});
ws.on('terminate', function(){
  logger.log('info', `Closing websocket connectio to ${csUrl}`);
  node.log(`Conexion terminada a ${csUrl}`);
  debug(`Conexion terminada a ${csUrl}`);
  node.status({fill: 'red', shape: 'dot', text: 'Cerrado...'});
  node.wsconnected = false;
});
ws.on('close', function() {
  logger.log('info', `Closing websocket connectio to ${csUrl}`);
  node.status({fill: 'red', shape: 'dot', text: 'Cerrado...'});
  node.wsconnected = false;
});
ws.on('error', function(err){

  node.log(`Error de Websocket: ${err}`);
  debug(`Error de Websocket: ${err}`);
});

ws.on('message', function(msgIn) {
  debug('Got a message ');
  let msg = {};
  msg.ocpp = {};
  msg.payload = {};

  msg.ocpp.ocppVersion = '1.6j';

  let response = [];
  let id = uuidv4();

  let msgParsed;

  if (msgIn[0] != '[') {
    msgParsed = JSON.parse('[' + msgIn + ']');
  } else {
    msgParsed = JSON.parse(msgIn);
  }

  logger.log(msgTypeStr[msgParsed[msgType]], JSON.stringify(msgParsed));

  if (msgParsed[msgType] == CALL) {
    debug(`Got a CALL Message ${msgParsed[msgId]}`);
    // msg.msgId = msgParsed[msgId];
    msg.msgId = id;
    msg.ocpp.MessageId = msgParsed[msgId];
    msg.ocpp.msgType = CALL;
    msg.ocpp.command = msgParsed[msgAction];
    msg.payload.command = msgParsed[msgAction];
    msg.payload.data = msgParsed[msgCallPayload];

    let to = setTimeout(function(id) {
      // node.log("kill:" + id);
      if (ee.listenerCount(id) > 0) {
        let evList = ee.listeners(id);
        let x = evList[0];
        ee.removeListener(id, x);
      }
    }, 1000, id);

    // This makes the response async so that we pass the responsibility onto the response node
    ee.once(id, function(returnMsg) {
      clearTimeout(to);
      response[msgType] = CALLRESULT;
      response[msgId] = msgParsed[msgId];
      response[msgResPayload] = returnMsg;

      logger.log(msgTypeStr[response[msgType]], JSON.stringify(response).replace(/,/g, ', '));

      ws.send(JSON.stringify(response));

    });
    node.status({fill: 'green', shape: 'dot', text: `message in: ${msg.ocpp.command}`});
    debug(`${ws.url} : message in: ${msg.ocpp.command}`);
    node.send(msg);
  } else if (msgParsed[msgType] == CALLRESULT) {
    debug(`Got a CALLRESULT msgId ${msgParsed[msgId]}`);
    msg.msgId = msgParsed[msgId];
    msg.ocpp.MessageId = msgParsed[msgId];
    msg.ocpp.msgType = CALLRESULT;
    msg.payload.data = msgParsed[msgResPayload];

    if (node.reqKV.hasOwnProperty(msg.msgId)){
      msg.ocpp.command = node.reqKV[msg.msgId];
      delete node.reqKV[msg.msgId];
    } else {
      msg.ocpp.command = 'unknown';
    }

    node.status({fill: 'green', shape: 'dot', text: `response in: ${msg.ocpp.command}`});
    debug(`response in: ${msg.ocpp.command}`);
    node.send(msg);

  }

});

ws.on('ping', function(){
  debug('Got Ping');
  ws.send('pong');
});
ws.on('pong', function(){
  debug('Got Pong');
});

this.on('input', function(msg) {

  if (node.wsconnected == true){

    let request = [];
    let messageTypeStr = ['unknown', 'unknown', 'request', 'replied', 'error'];

    debug(JSON.stringify(msg));

    request[msgType] = msg.payload.msgType || CALL;
    request[msgId] = msg.payload.MessageId || uuidv4();

    if (request[msgType] == CALL){
      request[msgAction] = msg.payload.command || node.command;

      if (!request[msgAction]){
        const errStr = 'ERROR: Missing Command in JSON request message';
        node.error(errStr);
        debug(errStr);
        return;
      }

      let cmddata;
      if (node.cmddata){
        try {
          cmddata = JSON.parse(node.cmddata);
        } catch (e){
          node.warn('OCPP JSON client node invalid payload.data for message (' + msg.ocpp.command + '): ' + e.message);
          return;
        }

      }

      request[msgCallPayload] = msg.payload.data || cmddata || {};
      if (!request[msgCallPayload]){
        const errStr = 'ERROR: Missing Data in JSON request message';
        node.error(errStr);
        debug(errStr);
        return;
      }

      node.reqKV[request[msgId]] = request[msgAction];
      debug(`Sending message: ${request[msgAction]}, ${request}`);
      node.status({fill: 'green', shape: 'dot', text: `request out: ${request[msgAction]}`});
    } else {
      request[msgResPayload] = msg.payload.data || {};
      debug(`Sending response message: ${JSON.stringify(request[msgResPayload])}`);
      node.status({fill: 'green', shape: 'dot', text: 'sending response'});
    }

    logger.log(messageTypeStr[request[msgType]], JSON.stringify(request).replace(/,/g, ', '));

    ws.send(JSON.stringify(request));
  }
});

this.on('close', function(){
  node.status({fill: 'red', shape: 'dot', text: 'Closed...'});
  logger.log('info', 'Websocket closed');
  debug('Closing CP client JSON node..');
  ws.close();
});

} // register our node RED.nodes.registerType('CP client JSON', OCPPChargePointJNode); };`

Thank you very much for your possible help

Cheers!

TheCaffinatedCoder commented 4 years ago

It's url, type, options, not url, options in your script: let rws = new ReconnectingWebSocket(csUrl, options); should be: let ws = new ReconnectingWebSocket(csUrl, [], options); The reason why we add [] is because [] is the websocket "protocol/type" which in my script is graphql-ws but in your case may be something different. We change rws to ws because your script uses the variable ws.on... instead of rws.on... Since you have WebSocket defined as WS, you also don't need const WebSocket = require('ws'); as far as I can tell either. You could probably get away with removing either the WS constant or WebSocket constant as long as you set WebSocket in configuration to a valid constant or variable that points to require('ws')

TheCaffinatedCoder commented 4 years ago

This library basically adds automatic reconnection and passes through everything you pass to it to what you define as WebSocket, so you don't need to use it directly, just tell the reconnecting websocket what to do instead. You also seem to be connecting to multiple nodes at once with a single websocket from what I can see, not sure about that but what I said would apply to a single connection.

mastin-zgz commented 4 years ago

Good morning, first of all, thank you very much for your help, I have made the suggested changes and the error has disappeared. Now it tells me: TypeError: ws.on is not a function

My code is this currently

`const WS = require('ws'); const uuidv4 = require('uuid/v4'); const events = require('events'); const EventEmitter = events.EventEmitter; const Logger = require('./utils/logdata'); const debug = require('debug')('anl:ocpp:cp:server:json'); const ReconnectingWebSocket = require('reconnecting-websocket'); let ee = new EventEmitter(); module.exports = function(RED) { 'use strict'; function OCPPChargePointJNode(config) { RED.nodes.createNode(this, config); debug('Starting CP client JSON node'); const CALL = 2; const CALLRESULT = 3; // for logging... const msgTypeStr = ['unknown', 'unknown', 'received', 'replied', 'error']; const msgType = 0; const msgId = 1; const msgAction = 2; const msgCallPayload = 3; const msgResPayload = 2; var obj = require('./global.json'); var node = this;

node.reqKV = {};

this.remotecs = RED.nodes.getNode(config.remotecs);
this.url = obj.url;
this.cbId = obj.cbId;
this.ocppVer = this.ocppver;
this.name = obj.nombre || config.name || this.remotecs.name;
this.command = config.command;
this.cmddata = config.cmddata;
this.logging = config.log || false;
this.pathlog = config.pathlog;

const options = {
    WebSocket: WS, // custom WebSocket constructor
    connectionTimeout: 1000,
    maxRetries: 10,
};

const logger = new Logger(this, this.pathlog, this.name);
logger.enabled = (this.logging && (typeof this.pathlog === 'string') && this.pathlog !== '');

let csUrl = `${this.url}/${this.cbId}`;

logger.log('info', `Realizando conexion websocket a ${csUrl}`);

let ws = new ReconnectingWebSocket(csUrl, [], options);

ws.on('open', function(){
  logger.log('info', `Connectado a ${csUrl}`);
  node.log(`Connectado a ${csUrl}`);
  debug(`Connectado a ${csUrl}`);
  node.status({fill: 'green', shape: 'dot', text: 'Conectado...'});
  node.wsconnected = true;
});
ws.on('terminate', function(){
  logger.log('info', `Closing websocket connectio to ${csUrl}`);
  node.log(`Conexion terminada a ${csUrl}`);
  debug(`Conexion terminada a ${csUrl}`);
  node.status({fill: 'red', shape: 'dot', text: 'Cerrado...'});
  node.wsconnected = false;
});
ws.on('close', function() {
  logger.log('info', `Closing websocket connectio to ${csUrl}`);
  node.status({fill: 'red', shape: 'dot', text: 'Cerrado...'});
  node.wsconnected = false;
});
ws.on('error', function(err){

  node.log(`Error de Websocket: ${err}`);
  debug(`Error de Websocket: ${err}`);
});

ws.on('message', function(msgIn) {
  debug('Got a message ');
  let msg = {};
  msg.ocpp = {};
  msg.payload = {};

  msg.ocpp.ocppVersion = '1.6j';

  let response = [];
  let id = uuidv4();

  let msgParsed;

  if (msgIn[0] != '[') {
    msgParsed = JSON.parse('[' + msgIn + ']');
  } else {
    msgParsed = JSON.parse(msgIn);
  }

  logger.log(msgTypeStr[msgParsed[msgType]], JSON.stringify(msgParsed));

  if (msgParsed[msgType] == CALL) {
    debug(`Got a CALL Message ${msgParsed[msgId]}`);
    // msg.msgId = msgParsed[msgId];
    msg.msgId = id;
    msg.ocpp.MessageId = msgParsed[msgId];
    msg.ocpp.msgType = CALL;
    msg.ocpp.command = msgParsed[msgAction];
    msg.payload.command = msgParsed[msgAction];
    msg.payload.data = msgParsed[msgCallPayload];

    let to = setTimeout(function(id) {
      // node.log("kill:" + id);
      if (ee.listenerCount(id) > 0) {
        let evList = ee.listeners(id);
        let x = evList[0];
        ee.removeListener(id, x);
      }
    }, 1000, id);

    // This makes the response async so that we pass the responsibility onto the response node
    ee.once(id, function(returnMsg) {
      clearTimeout(to);
      response[msgType] = CALLRESULT;
      response[msgId] = msgParsed[msgId];
      response[msgResPayload] = returnMsg;

      logger.log(msgTypeStr[response[msgType]], JSON.stringify(response).replace(/,/g, ', '));

      ws.send(JSON.stringify(response));

    });
    node.status({fill: 'green', shape: 'dot', text: `message in: ${msg.ocpp.command}`});
    debug(`${ws.url} : message in: ${msg.ocpp.command}`);
    node.send(msg);
  } else if (msgParsed[msgType] == CALLRESULT) {
    debug(`Got a CALLRESULT msgId ${msgParsed[msgId]}`);
    msg.msgId = msgParsed[msgId];
    msg.ocpp.MessageId = msgParsed[msgId];
    msg.ocpp.msgType = CALLRESULT;
    msg.payload.data = msgParsed[msgResPayload];

    if (node.reqKV.hasOwnProperty(msg.msgId)){
      msg.ocpp.command = node.reqKV[msg.msgId];
      delete node.reqKV[msg.msgId];
    } else {
      msg.ocpp.command = 'unknown';
    }

    node.status({fill: 'green', shape: 'dot', text: `response in: ${msg.ocpp.command}`});
    debug(`response in: ${msg.ocpp.command}`);
    node.send(msg);

  }

});

ws.on('ping', function(){
  debug('Got Ping');
  ws.send('pong');
});
ws.on('pong', function(){
  debug('Got Pong');
});

this.on('input', function(msg) {

  if (node.wsconnected == true){

    let request = [];
    let messageTypeStr = ['unknown', 'unknown', 'request', 'replied', 'error'];

    debug(JSON.stringify(msg));

    request[msgType] = msg.payload.msgType || CALL;
    request[msgId] = msg.payload.MessageId || uuidv4();

    if (request[msgType] == CALL){
      request[msgAction] = msg.payload.command || node.command;

      if (!request[msgAction]){
        const errStr = 'ERROR: Missing Command in JSON request message';
        node.error(errStr);
        debug(errStr);
        return;
      }

      let cmddata;
      if (node.cmddata){
        try {
          cmddata = JSON.parse(node.cmddata);
        } catch (e){
          node.warn('OCPP JSON client node invalid payload.data for message (' + msg.ocpp.command + '): ' + e.message);
          return;
        }

      }

      request[msgCallPayload] = msg.payload.data || cmddata || {};
      if (!request[msgCallPayload]){
        const errStr = 'ERROR: Missing Data in JSON request message';
        node.error(errStr);
        debug(errStr);
        return;
      }

      node.reqKV[request[msgId]] = request[msgAction];
      debug(`Sending message: ${request[msgAction]}, ${request}`);
      node.status({fill: 'green', shape: 'dot', text: `request out: ${request[msgAction]}`});
    } else {
      request[msgResPayload] = msg.payload.data || {};
      debug(`Sending response message: ${JSON.stringify(request[msgResPayload])}`);
      node.status({fill: 'green', shape: 'dot', text: 'sending response'});
    }

    logger.log(messageTypeStr[request[msgType]], JSON.stringify(request).replace(/,/g, ', '));

    ws.send(JSON.stringify(request));
  }
});

this.on('close', function(){
  node.status({fill: 'red', shape: 'dot', text: 'Closed...'});
  logger.log('info', 'Websocket closed');
  debug('Closing CP client JSON node..');
  ws.close();
});

} // register our node RED.nodes.registerType('CP client JSON', OCPPChargePointJNode); };`

again, thank you very much for your time. Cheers

shamoons commented 4 years ago

I'm unsure what the solution to this is

chrisn-au commented 4 years ago

@shamoons, In my case in the options was to change

Constructor : ws

to

WebSocket : ws.

@mastin-zgz

I needed to change the .on to .addEventListener and the code ran. Funny enough I am also using the ocpp node red client. Once I get it working I will create a pull request to ocpp node red to update it.

Hope this helps

chrisn-au commented 4 years ago

@mastin-zgz I have created a pull request to the node-red-contrib-ocpp library but in the mean time to get this working, you will need to

change occurrence of ws.on to ws.addEventListener

and in

  ws.addEventListener('message', function(msgInRaw) { // change msgIn to msgInRaw
      debug('Got a message ');
      let msgIn = msgInRaw.data // add this line 

there may be other issues but they should be raised with node-red-ocpp not @TheCaffinatedCoder library.

For reference

PR on node-red-contrib-ocpp

A docker image containing the changes and a trivial OCPP start flow

I hope this helps

shamoons commented 4 years ago

@chrisn-au , I'm trying:

            ws.addEventListener('close', (code, reason) => {
              console.log('Event: close', code, reason)
              EventHandler('close', resStream.id, resStream.postTo, ws, { code, reason })
            })

but getting:

Argument of type '(code: any, reason: any) => void' is not assignable to parameter of type '(event: CloseEvent) => void | { handleEvent: (event: CloseEvent) => void; }'.
chrisn-au commented 4 years ago

Ok, the addEventListener callback simply takes an event object as a parameter. So I believe it should be

ws.addEventListener('close', (event) => {
              let code = event.code;
              let reason = event.reason;
              console.log('Event: close', code, reason)
              EventHandler('close', resStream.id, resStream.postTo, ws, { code, reason })
            })

Let me know how it goes

mastin-zgz commented 4 years ago

Now it's going great!

It already reconnects automatically perfectly, the fault was that it did not have to put anything

ws.on else. ws.addEventListener Thank you very much for the help!

Makeshift commented 3 years ago

I'm also confused as how to actually use this library. An incredibly basic invocation that I'd expect to work given the docs is:

const WS = require("ws");
const ReconnectingWebSocket = require("reconnecting-websocket");
const ws = new ReconnectingWebSocket("wss://<url>/", { WebSocket: WS });

However, this fails with the error in the title.

It appears that no matter what, getGlobalWebSocket is called during connection, which is weird as I just tested this syntax and it works in vanilla JavaScript at least (that is, getGlobalWebSocket is not called if this._options has a populated WebSocket:

const {
            maxRetries = DEFAULT.maxRetries,
            connectionTimeout = DEFAULT.connectionTimeout,
            WebSocket = getGlobalWebSocket(),
        } = this._options;

Unfortunately I'm not really able to dig into this as the compiled JS is horrible to debug and I haven't got anything set up to debug TS, but it appears that _options isn't available within the _connect function.

EDIT:

Nevermind, I'm just dumb.

const WS = require("ws");
const ReconnectingWebSocket = require("reconnecting-websocket");
const ws = new ReconnectingWebSocket("wss://<url>/", [], { WebSocket: WS });