X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2FChargingStation.js;h=be4f161380c6fad845520e4cb2b5139fe79fd621;hb=d20c21a0906551f6d22f07688ca2cd9b10bbd84b;hp=daa9475f44d2907218547049729c76105b50acbe;hpb=2e6f5966ecbefada47d60b1b53d21fe49be439a5;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStation.js b/src/charging-station/ChargingStation.js index daa9475f..be4f1613 100644 --- a/src/charging-station/ChargingStation.js +++ b/src/charging-station/ChargingStation.js @@ -4,7 +4,6 @@ const WebSocket = require('ws'); const Constants = require('../utils/Constants'); const Utils = require('../utils/Utils'); const OCPPError = require('./OcppError'); -const {v4: uuid} = require('uuid'); const AutomaticTransactionGenerator = require('./AutomaticTransactionGenerator'); const Statistics = require('../utils/Statistics'); const fs = require('fs'); @@ -24,6 +23,8 @@ class ChargingStation { this._messageQueue = []; this._isSocketRestart = false; + + this._authorizedTags = this._loadAndGetAuthorizedTags(); } _initialize() { @@ -31,9 +32,10 @@ class ChargingStation { this._bootNotificationMessage = { chargePointModel: this._stationInfo.chargePointModel, chargePointVendor: this._stationInfo.chargePointVendor, + chargePointSerialNumber: this._stationInfo.chargePointSerialNumberPrefix ? this._stationInfo.chargePointSerialNumberPrefix : '', + firmwareVersion: this._stationInfo.firmwareVersion ? this._stationInfo.firmwareVersion : '', }; this._configuration = this._getConfiguration(); - this._authorizedTags = this._getAuthorizedTags(); this._supervisionUrl = this._getSupervisionURL(); this._statistics = new Statistics(this._stationInfo.name); this._performanceObserver = new PerformanceObserver((list) => { @@ -55,7 +57,7 @@ class ChargingStation { return this._stationInfo.authorizationFile ? this._stationInfo.authorizationFile : ''; } - _getAuthorizedTags() { + _loadAndGetAuthorizedTags() { let authorizedTags = []; const authorizationFile = this._getAuthorizationFile(); if (authorizationFile) { @@ -79,7 +81,7 @@ class ChargingStation { try { logger.debug(this._basicFormatLog() + ' Authorization file ' + this._getAuthorizationFile() + ' have changed, reload'); // Initialize _authorizedTags - this._authorizedTags = this._getAuthorizedTags(); + this._authorizedTags = this._loadAndGetAuthorizedTags(); } catch (error) { logger.error(this._basicFormatLog() + ' Authorization file monitoring error: ' + error); } @@ -119,12 +121,12 @@ class ChargingStation { } _getAuthorizeRemoteTxRequests() { - const authorizeRemoteTxRequests = this._configuration.configurationKey.find((configElement) => configElement.key === 'AuthorizeRemoteTxRequests'); + const authorizeRemoteTxRequests = this._getConfigurationKey('AuthorizeRemoteTxRequests'); return authorizeRemoteTxRequests ? Utils.convertToBoolean(authorizeRemoteTxRequests.value) : false; } _getLocalAuthListEnabled() { - const localAuthListEnabled = this._configuration.configurationKey.find((configElement) => configElement.key === 'LocalAuthListEnabled'); + const localAuthListEnabled = this._getConfigurationKey('LocalAuthListEnabled'); return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false; } @@ -149,13 +151,13 @@ class ChargingStation { } async start() { + this._url = this._supervisionUrl + '/' + this._stationInfo.name; + this._wsConnection = new WebSocket(this._url, 'ocpp1.6'); logger.info(this._basicFormatLog() + ' Will communicate with ' + this._supervisionUrl); // Monitor authorization file this._startAuthorizationFileMonitoring(); // Monitor station template file this._startStationTemplateFileMonitoring(); - this._url = this._supervisionUrl + '/' + this._stationInfo.name; - this._wsConnection = new WebSocket(this._url, 'ocpp1.6'); // Handle Socket incoming messages this._wsConnection.on('message', this.onMessage.bind(this)); // Handle Socket error @@ -170,8 +172,16 @@ class ChargingStation { onOpen() { logger.info(`${this._basicFormatLog()} Is connected to server through ${this._url}`); + if (!this._heartbeatInterval) { + // Send BootNotification + try { + this.sendMessage(Utils.generateUUID(), this._bootNotificationMessage, Constants.OCPP_JSON_CALL_MESSAGE, 'BootNotification'); + } catch (error) { + logger.error(this._basicFormatLog() + ' Send boot notification error: ' + error); + } + } if (this._isSocketRestart) { - this.basicStartMessageSequence(); + this._basicStartMessageSequence(); if (this._messageQueue.length > 0) { this._messageQueue.forEach((message) => { if (this._wsConnection.readyState === WebSocket.OPEN) { @@ -179,13 +189,6 @@ class ChargingStation { } }); } - } else { - // At first start, send BootNotification - try { - this.sendMessage(uuid(), this._bootNotificationMessage, Constants.OCPP_JSON_CALL_MESSAGE, 'BootNotification'); - } catch (error) { - logger.error(this._basicFormatLog() + ' Send boot notification error: ' + error); - } } this._autoReconnectRetryCount = 0; this._isSocketRestart = false; @@ -218,7 +221,7 @@ class ChargingStation { } onPing() { - logger.info(this._basicFormatLog() + ' Has received a WS ping (rfc6455) from the server'); + logger.debug(this._basicFormatLog() + ' Has received a WS ping (rfc6455) from the server'); } async onMessage(message) { @@ -232,7 +235,6 @@ class ChargingStation { // Incoming Message case Constants.OCPP_JSON_CALL_MESSAGE: // Process the call - this._statistics.addMessage(commandName); await this.handleRequest(messageId, commandName, commandPayload); break; // Outcome Message @@ -250,7 +252,6 @@ class ChargingStation { throw new Error(`Response for unknown message id ${messageId}`); } delete this._requests[messageId]; - // this._statistics.addMessage(commandName) responseCallback(commandName, requestPayload); break; // Error Message @@ -281,16 +282,37 @@ class ChargingStation { } } + // eslint-disable-next-line class-methods-use-this + async _startHeartbeat(self) { + if (self._heartbeatInterval && !self._heartbeatSetInterval) { + self._heartbeatSetInterval = setInterval(() => { + try { + const payload = { + currentTime: new Date().toISOString(), + }; + self.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'Heartbeat'); + } catch (error) { + logger.error(self._basicFormatLog() + ' Send heartbeat error: ' + error); + } + }, self._heartbeatInterval); + logger.info(self._basicFormatLog() + ' Heartbeat started every ' + self._heartbeatInterval + 'ms'); + } else { + logger.error(self._basicFormatLog() + ' Heartbeat interval undefined, not starting the heartbeat'); + } + } + _reconnect(error) { logger.error(this._basicFormatLog() + ' Socket: abnormally closed', error); - // Stop heartbeat interval + // Stop heartbeat if (this._heartbeatSetInterval) { clearInterval(this._heartbeatSetInterval); this._heartbeatSetInterval = null; } - // Stop the ATG - if (this._stationInfo.AutomaticTransactionGenerator.enable && this._automaticTransactionGeneration && - !this._automaticTransactionGeneration._timeToStop) { + // Stop the ATG if needed + if (Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.enable) && + Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.stopOnConnectionFailure) && + this._automaticTransactionGeneration && + !this._automaticTransactionGeneration.timeToStop) { this._automaticTransactionGeneration.stop(); } if (this._autoReconnectTimeout !== 0 && @@ -306,18 +328,30 @@ class ChargingStation { } } - send(command, messageType = Constants.OCPP_JSON_CALL_MESSAGE) { - // Send Message - return this.sendMessage(uuid(), command, messageType); - } - sendError(messageId, err) { // Check exception: only OCPP error are accepted - const error = (err instanceof OCPPError ? err : new OCPPError(Constants.OCPP_ERROR_INTERNAL_ERROR, err.message)); + const error = err instanceof OCPPError ? err : new OCPPError(Constants.OCPP_ERROR_INTERNAL_ERROR, err.message); // Send error return this.sendMessage(messageId, error, Constants.OCPP_JSON_CALL_ERROR_MESSAGE); } + async sendStatusNotification(connectorId, status, errorCode = 'NoError') { + try { + const payload = { + connectorId, + errorCode, + status, + }; + await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StatusNotification'); + } catch (error) { + logger.error(this._basicFormatLog() + ' Send status error: ' + error); + } + } + + async sendStatusNotificationWithTimeout(connectorId, status, errorCode = 'NoError', timeout = Constants.STATUS_NOTIFICATION_TIMEOUT) { + setTimeout(() => this.sendStatusNotification(connectorId, status, errorCode), timeout); + } + sendMessage(messageId, command, messageType = Constants.OCPP_JSON_CALL_RESULT_MESSAGE, commandName = '') { // Send a message through wsConnection const self = this; @@ -335,13 +369,14 @@ class ChargingStation { break; // Response case Constants.OCPP_JSON_CALL_RESULT_MESSAGE: + this._statistics.addMessage(commandName); // Build response messageToSend = JSON.stringify([messageType, messageId, command]); break; // Error Message case Constants.OCPP_JSON_CALL_ERROR_MESSAGE: // Build Message - this._statistics.addMessage(`Error ${command.code}`); + this._statistics.addMessage(`Error ${command.code ? command.code : Constants.OCPP_ERROR_GENERIC_ERROR} on ${commandName || ''}`); messageToSend = JSON.stringify([messageType, messageId, command.code ? command.code : Constants.OCPP_ERROR_GENERIC_ERROR, command.message ? command.message : '', command.details ? command.details : {}]); break; } @@ -370,7 +405,7 @@ class ChargingStation { if (typeof self[responseCallbackFn] === 'function') { self[responseCallbackFn](payload, requestPayload, self); } else { - logger.debug(self._basicFormatLog() + ' Trying to call an undefined callback function: ' + responseCallbackFn); + logger.debug(self._basicFormatLog() + ' Trying to call an undefined response callback function: ' + responseCallbackFn); } // Send the response resolve(payload); @@ -378,6 +413,7 @@ class ChargingStation { // Function that will receive the request's rejection function rejectCallback(reason) { + self._statistics.addMessage(`Error ${command.code ? command.code : Constants.OCPP_ERROR_GENERIC_ERROR} on ${commandName || ''}`, true); // Build Exception // eslint-disable-next-line no-empty-function self._requests[messageId] = [() => { }, () => { }, '']; // Properly format the request @@ -388,39 +424,33 @@ class ChargingStation { }); } - handleResponseBootNotification(payload) { - if (payload.status === 'Accepted') { - this._heartbeatInterval = payload.interval * 1000; - this.basicStartMessageSequence(); - } else { - logger.info(this._basicFormatLog() + ' Boot Notification rejected'); - } - } - - async basicStartMessageSequence() { + async _basicStartMessageSequence() { + // Start heartbeat this._startHeartbeat(this); - // build connectors + // Build connectors if (!this._connectors) { this._connectors = {}; const connectorsConfig = Utils.cloneJSonDocument(this._stationInfo.Connectors); - // determine number of customized connectors + // Determine number of customized connectors let lastConnector; for (lastConnector in connectorsConfig) { - // add connector 0, OCPP specification violation that for example KEBA have - if (Utils.convertToInt(lastConnector) === 0 && this._stationInfo.useConnectorId0) { + // Add connector 0, OCPP specification violation that for example KEBA have + if (Utils.convertToInt(lastConnector) === 0 && Utils.convertToBoolean(this._stationInfo.useConnectorId0) && + connectorsConfig[lastConnector]) { this._connectors[lastConnector] = connectorsConfig[lastConnector]; } } let maxConnectors = 0; if (Array.isArray(this._stationInfo.numberOfConnectors)) { - // generate some connectors + // Generate some connectors maxConnectors = this._stationInfo.numberOfConnectors[(this._index - 1) % this._stationInfo.numberOfConnectors.length]; } else { maxConnectors = this._stationInfo.numberOfConnectors; } - // generate all connectors + this._addConfigurationKey('NumberOfConnectors', maxConnectors, true); + // Generate all connectors for (let index = 1; index <= maxConnectors; index++) { - const randConnectorID = this._stationInfo.randomConnectors ? Utils.getRandomInt(maxConnectors, 1) : index; + const randConnectorID = Utils.convertToBoolean(this._stationInfo.randomConnectors) ? Utils.getRandomInt(lastConnector, 1) : index; this._connectors[index] = connectorsConfig[randConnectorID]; } } @@ -428,87 +458,115 @@ class ChargingStation { for (const connector in this._connectors) { if (!this._connectors[connector].transactionStarted) { if (this._connectors[connector].bootStatus) { - setTimeout(() => this.sendStatusNotification(connector, this._connectors[connector].bootStatus), Constants.STATUS_NOTIFICATION_TIMEOUT); + this.sendStatusNotificationWithTimeout(connector, this._connectors[connector].bootStatus); } else { - setTimeout(() => this.sendStatusNotification(connector, 'Available'), Constants.STATUS_NOTIFICATION_TIMEOUT); + this.sendStatusNotificationWithTimeout(connector, 'Available'); } } else { - setTimeout(() => this.sendStatusNotification(connector, 'Charging'), Constants.STATUS_NOTIFICATION_TIMEOUT); + this.sendStatusNotificationWithTimeout(connector, 'Charging'); } } - if (this._stationInfo.AutomaticTransactionGenerator.enable) { + if (Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.enable)) { if (!this._automaticTransactionGeneration) { this._automaticTransactionGeneration = new AutomaticTransactionGenerator(this); } - this._automaticTransactionGeneration.start(); + if (this._automaticTransactionGeneration.timeToStop) { + this._automaticTransactionGeneration.start(); + } } this._statistics.start(); } + _resetTransactionOnConnector(connectorID) { + this._connectors[connectorID].transactionStarted = false; + this._connectors[connectorID].transactionId = null; + this._connectors[connectorID].idTag = null; + this._connectors[connectorID].lastConsumptionValue = -1; + this._connectors[connectorID].lastSoC = -1; + if (this._connectors[connectorID].transactionSetInterval) { + clearInterval(this._connectors[connectorID].transactionSetInterval); + } + } + + handleResponseBootNotification(payload) { + if (payload.status === 'Accepted') { + this._heartbeatInterval = payload.interval * 1000; + this._addConfigurationKey('HeartBeatInterval', Utils.convertToInt(payload.interval)); + this._addConfigurationKey('HeartbeatInterval', Utils.convertToInt(payload.interval), false, false); + this._basicStartMessageSequence(); + } else { + logger.info(this._basicFormatLog() + ' Boot Notification rejected'); + } + } + handleResponseStartTransaction(payload, requestPayload) { - // Reset connector transaction related attributes - this._connectors[requestPayload.connectorId].transactionStarted = false; - this._connectors[requestPayload.connectorId].idTag = requestPayload.idTag; - - if (payload.idTagInfo.status === 'Accepted') { - for (const connector in this._connectors) { - if (Utils.convertToInt(connector) === Utils.convertToInt(requestPayload.connectorId)) { - this._connectors[connector].transactionStarted = true; - this._connectors[connector].transactionId = payload.transactionId; - this._connectors[connector].lastConsumptionValue = 0; - this._connectors[connector].lastSoC = 0; - logger.info(this._basicFormatLog() + ' Transaction ' + this._connectors[connector].transactionId + ' STARTED on ' + this._stationInfo.name + '#' + requestPayload.connectorId + ' with idTag ' + requestPayload.idTag); - this.sendStatusNotification(requestPayload.connectorId, 'Charging'); - const configuredMeterValueSampleInterval = this._configuration.configurationKey.find((value) => value.key === 'MeterValueSampleInterval'); - this.startMeterValues(requestPayload.connectorId, - (configuredMeterValueSampleInterval ? configuredMeterValueSampleInterval.value * 1000 : 60000), - this); - } + if (this._connectors[requestPayload.connectorId].transactionStarted) { + logger.debug(this._basicFormatLog() + ' Try to start a transaction on an already used connector ' + requestPayload.connectorId + ' by transaction ' + this._connectors[requestPayload.connectorId].transactionId); + } + + let transactionConnectorId; + for (const connector in this._connectors) { + if (Utils.convertToInt(connector) === Utils.convertToInt(requestPayload.connectorId)) { + transactionConnectorId = connector; + break; } + } + if (!transactionConnectorId) { + logger.error(this._basicFormatLog() + ' Try to start a transaction on a non existing connector Id ' + requestPayload.connectorId); + return; + } + if (payload.idTagInfo && payload.idTagInfo.status === 'Accepted') { + this._connectors[transactionConnectorId].transactionStarted = true; + this._connectors[transactionConnectorId].transactionId = payload.transactionId; + this._connectors[transactionConnectorId].idTag = requestPayload.idTag; + this._connectors[transactionConnectorId].lastConsumptionValue = 0; + this._connectors[transactionConnectorId].lastSoC = 0; + this.sendStatusNotification(requestPayload.connectorId, 'Charging'); + logger.info(this._basicFormatLog() + ' Transaction ' + this._connectors[transactionConnectorId].transactionId + ' STARTED on ' + this._stationInfo.name + '#' + requestPayload.connectorId + ' for idTag ' + requestPayload.idTag); + const configuredMeterValueSampleInterval = this._getConfigurationKey('MeterValueSampleInterval'); + this.startMeterValues(requestPayload.connectorId, + configuredMeterValueSampleInterval ? configuredMeterValueSampleInterval.value * 1000 : 60000); } else { logger.error(this._basicFormatLog() + ' Starting transaction id ' + payload.transactionId + ' REJECTED with status ' + payload.idTagInfo.status + ', idTag ' + requestPayload.idTag); - for (const connector in this._connectors) { - if (Utils.convertToInt(connector) === Utils.convertToInt(requestPayload.connectorId)) { - this._resetTransactionOnConnector(connector); - } - } + this._resetTransactionOnConnector(transactionConnectorId); this.sendStatusNotification(requestPayload.connectorId, 'Available'); } } - async sendStatusNotification(connectorId, status, errorCode = 'NoError') { - try { - const payload = { - connectorId, - errorCode, - status, - }; - await this.sendMessage(uuid(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StatusNotification'); - } catch (error) { - logger.error(this._basicFormatLog() + ' Send status error: ' + error); + handleResponseStopTransaction(payload, requestPayload) { + let transactionConnectorId; + for (const connector in this._connectors) { + if (this._connectors[connector].transactionId === requestPayload.transactionId) { + transactionConnectorId = connector; + break; + } } - } - - // eslint-disable-next-line class-methods-use-this - async _startHeartbeat(self) { - if (self._heartbeatInterval && !self._heartbeatSetInterval) { - logger.info(self._basicFormatLog() + ' Heartbeat started every ' + self._heartbeatInterval + 'ms'); - self._heartbeatSetInterval = setInterval(() => { - try { - const payload = { - currentTime: new Date().toISOString(), - }; - self.sendMessage(uuid(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'Heartbeat'); - } catch (error) { - logger.error(self._basicFormatLog() + ' Send heartbeat error: ' + error); - } - }, self._heartbeatInterval); + if (!transactionConnectorId) { + logger.error(this._basicFormatLog() + ' Try to stop a non existing transaction ' + requestPayload.transactionId); + return; + } + if (payload.idTagInfo && payload.idTagInfo.status === 'Accepted') { + this.sendStatusNotification(transactionConnectorId, 'Available'); + logger.info(this._basicFormatLog() + ' Transaction ' + this._connectors[transactionConnectorId].transactionId + ' STOPPED on ' + this._stationInfo.name + '#' + transactionConnectorId); + this._resetTransactionOnConnector(transactionConnectorId); } else { - logger.error(self._basicFormatLog() + ' Heartbeat interval undefined, not starting the heartbeat'); + logger.error(this._basicFormatLog() + ' Stopping transaction id ' + this._connectors[transactionConnectorId].transactionId + ' REJECTED with status ' + payload.idTagInfo.status); } } + handleResponseStatusNotification(payload, requestPayload) { + logger.debug(this._basicFormatLog() + ' Status notification response received: %j to status notification request: %j', payload, requestPayload); + } + + handleResponseMeterValues(payload, requestPayload) { + logger.debug(this._basicFormatLog() + ' MeterValues response received: %j to MeterValues request: %j', payload, requestPayload); + } + + handleResponseHeartbeat(payload, requestPayload) { + logger.debug(this._basicFormatLog() + ' Heartbeat response received: %j to Heartbeat request: %j', payload, requestPayload); + } + async handleRequest(messageId, commandName, commandPayload) { let result; this._statistics.addMessage(commandName, true); @@ -520,7 +578,7 @@ class ChargingStation { } catch (error) { // Log logger.error(this._basicFormatLog() + ' Handle request error: ' + error); - // Send back response to inform back end + // Send back response to inform backend await this.sendError(messageId, error); } } else { @@ -532,39 +590,143 @@ class ChargingStation { await this.sendMessage(messageId, result, Constants.OCPP_JSON_CALL_RESULT_MESSAGE); } - async handleGetConfiguration() { - return this._configuration; + _getConfigurationKey(key) { + return this._configuration.configurationKey.find((configElement) => configElement.key === key); + } + + _addConfigurationKey(key, value, readonly = false, visible = true, reboot = false) { + const keyFound = this._getConfigurationKey(key); + if (!keyFound) { + this._configuration.configurationKey.push({ + key, + readonly, + value, + visible, + reboot, + }); + } + } + + _setConfigurationKeyValue(key, value) { + const keyFound = this._getConfigurationKey(key); + if (keyFound) { + const keyIndex = this._configuration.configurationKey.indexOf(keyFound); + this._configuration.configurationKey[keyIndex].value = value; + } + } + + async handleGetConfiguration(commandPayload) { + const configurationKey = []; + const unknownKey = []; + if (Utils.isEmptyArray(commandPayload.key)) { + for (const configuration of this._configuration.configurationKey) { + if (Utils.isUndefined(configuration.visible)) { + configuration.visible = true; + } else { + configuration.visible = Utils.convertToBoolean(configuration.visible); + } + if (!configuration.visible) { + continue; + } + configurationKey.push({ + key: configuration.key, + readonly: configuration.readonly, + value: configuration.value, + }); + } + } else { + for (const configurationKey of commandPayload.key) { + const keyFound = this._getConfigurationKey(configurationKey); + if (keyFound) { + if (Utils.isUndefined(keyFound.visible)) { + keyFound.visible = true; + } else { + keyFound.visible = Utils.convertToBoolean(configurationKey.visible); + } + if (!keyFound.visible) { + continue; + } + configurationKey.push({ + key: keyFound.key, + readonly: keyFound.readonly, + value: keyFound.value, + }); + } else { + unknownKey.push(configurationKey); + } + } + } + return { + configurationKey, + unknownKey, + }; } async handleChangeConfiguration(commandPayload) { - const keyToChange = this._configuration.configurationKey.find((element) => element.key === commandPayload.key); - if (keyToChange && !Utils.convertToBoolean(keyToChange.readonly)) { + const keyToChange = this._getConfigurationKey(commandPayload.key); + if (!keyToChange) { + return {status: Constants.OCPP_ERROR_NOT_SUPPORTED}; + } else if (keyToChange && Utils.convertToBoolean(keyToChange.readonly)) { + return Constants.OCPP_RESPONSE_REJECTED; + } else if (keyToChange && !Utils.convertToBoolean(keyToChange.readonly)) { const keyIndex = this._configuration.configurationKey.indexOf(keyToChange); this._configuration.configurationKey[keyIndex].value = commandPayload.value; + let triggerHeartbeatRestart = false; + if (keyToChange.key === 'HeartBeatInterval') { + this._setConfigurationKeyValue('HeartbeatInterval', commandPayload.value); + triggerHeartbeatRestart = true; + } + if (keyToChange.key === 'HeartbeatInterval') { + this._setConfigurationKeyValue('HeartBeatInterval', commandPayload.value); + triggerHeartbeatRestart = true; + } + if (triggerHeartbeatRestart) { + this._heartbeatInterval = Utils.convertToInt(commandPayload.value) * 1000; + // Stop heartbeat + if (this._heartbeatSetInterval) { + clearInterval(this._heartbeatSetInterval); + this._heartbeatSetInterval = null; + } + // Start heartbeat + this._startHeartbeat(this); + } + if (Utils.convertToBoolean(keyToChange.reboot)) { + return Constants.OCPP_RESPONSE_REBOOT_REQUIRED; + } return Constants.OCPP_RESPONSE_ACCEPTED; } - return Constants.OCPP_RESPONSE_REJECTED; } async handleRemoteStartTransaction(commandPayload) { - const transactionConnectorID = (commandPayload.connectorId ? commandPayload.connectorId : '1'); + const transactionConnectorID = commandPayload.connectorId ? commandPayload.connectorId : '1'; if (this.hasAuthorizedTags() && this._getLocalAuthListEnabled() && this._getAuthorizeRemoteTxRequests()) { // Check if authorized if (this._authorizedTags.find((value) => value === commandPayload.idTag)) { // Authorization successful start transaction - setTimeout(() => this.sendStartTransaction(transactionConnectorID, commandPayload.idTag), Constants.START_TRANSACTION_TIMEOUT); - logger.debug(this._basicFormatLog() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID + ' with idTag ' + commandPayload.idTag); + this.sendStartTransactionWithTimeout(transactionConnectorID, commandPayload.idTag, Constants.START_TRANSACTION_TIMEOUT); + logger.debug(this._basicFormatLog() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID + ' for idTag ' + commandPayload.idTag); return Constants.OCPP_RESPONSE_ACCEPTED; } logger.error(this._basicFormatLog() + ' Remote starting transaction REJECTED with status ' + commandPayload.idTagInfo.status + ', idTag ' + commandPayload.idTag); return Constants.OCPP_RESPONSE_REJECTED; } // No local authorization check required => start transaction - setTimeout(() => this.sendStartTransaction(transactionConnectorID, commandPayload.idTag), Constants.START_TRANSACTION_TIMEOUT); - logger.debug(this._basicFormatLog() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID + ' with idTag ' + commandPayload.idTag); + this.sendStartTransactionWithTimeout(transactionConnectorID, commandPayload.idTag, Constants.START_TRANSACTION_TIMEOUT); + logger.debug(this._basicFormatLog() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID + ' for idTag ' + commandPayload.idTag); return Constants.OCPP_RESPONSE_ACCEPTED; } + async handleRemoteStopTransaction(commandPayload) { + for (const connector in this._connectors) { + if (this._connectors[connector].transactionId === commandPayload.transactionId) { + this.sendStopTransaction(commandPayload.transactionId); + return Constants.OCPP_RESPONSE_ACCEPTED; + } + } + logger.info(this._basicFormatLog() + ' Try to stop remotely a non existing transaction ' + commandPayload.transactionId); + return Constants.OCPP_RESPONSE_REJECTED; + } + async sendStartTransaction(connectorID, idTag) { try { const payload = { @@ -573,44 +735,32 @@ class ChargingStation { meterStart: 0, timestamp: new Date().toISOString(), }; - return await this.sendMessage(uuid(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StartTransaction'); + return await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StartTransaction'); } catch (error) { logger.error(this._basicFormatLog() + ' Send start transaction error: ' + error); - this._resetTransactionOnConnector(connectorID); throw error; } } + async sendStartTransactionWithTimeout(connectorID, idTag, timeout) { + setTimeout(() => this.sendStartTransaction(connectorID, idTag), timeout); + } - async sendStopTransaction(transactionId, connectorID) { + async sendStopTransaction(transactionId) { try { const payload = { transactionId, meterStop: 0, timestamp: new Date().toISOString(), }; - await this.sendMessage(uuid(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StopTransaction'); - logger.info(this._basicFormatLog() + ' Transaction ' + this._connectors[connectorID].transactionId + ' STOPPED on ' + this._stationInfo.name + '#' + connectorID); - this.sendStatusNotification(connectorID, 'Available'); + await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StopTransaction'); } catch (error) { logger.error(this._basicFormatLog() + ' Send stop transaction error: ' + error); throw error; - } finally { - this._resetTransactionOnConnector(connectorID); - } - } - - _resetTransactionOnConnector(connectorID) { - this._connectors[connectorID].transactionStarted = false; - this._connectors[connectorID].transactionId = null; - this._connectors[connectorID].lastConsumptionValue = -1; - this._connectors[connectorID].lastSoC = -1; - if (this._connectors[connectorID].transactionInterval) { - clearInterval(this._connectors[connectorID].transactionInterval); } } // eslint-disable-next-line class-methods-use-this - async sendMeterValues(connectorID, interval, self) { + async sendMeterValues(connectorID, interval, self, debug = false) { try { const sampledValueLcl = { timestamp: new Date().toISOString(), @@ -622,30 +772,25 @@ class ChargingStation { sampledValueLcl.sampledValue = [meterValuesClone]; } for (let index = 0; index < sampledValueLcl.sampledValue.length; index++) { + const connector = self._connectors[connectorID]; if (sampledValueLcl.sampledValue[index].measurand && sampledValueLcl.sampledValue[index].measurand === 'SoC') { - sampledValueLcl.sampledValue[index].value = Math.floor(Math.random() * 100) + 1; - if (sampledValueLcl.sampledValue[index].value > 100) { - logger.info(self._basicFormatLog() + ' Meter type: ' + - (sampledValueLcl.sampledValue[index].measurand ? sampledValueLcl.sampledValue[index].measurand : 'default') + - ' value: ' + sampledValueLcl.sampledValue[index].value); + sampledValueLcl.sampledValue[index].value = Utils.getRandomInt(100); + if (sampledValueLcl.sampledValue[index].value > 100 || debug) { + logger.error(`${self._basicFormatLog()} MeterValues measurand ${sampledValueLcl.sampledValue[index].measurand ? sampledValueLcl.sampledValue[index].measurand : 'Energy.Active.Import.Register'}: connectorID ${connectorID}, transaction ${connector.transactionId}, value: ${sampledValueLcl.sampledValue[index].value}`); } } else { // Persist previous value in connector - const connector = self._connectors[connectorID]; - let consumption; - consumption = Utils.getRandomInt(self._stationInfo.maxPower / 3600000 * interval, 4); + const consumption = Utils.getRandomInt(self._stationInfo.maxPower / 3600000 * interval); if (connector && connector.lastConsumptionValue >= 0) { connector.lastConsumptionValue += consumption; } else { connector.lastConsumptionValue = 0; } - consumption = Math.round(connector.lastConsumptionValue * 3600 / interval); - logger.info(self._basicFormatLog() + ' ConnectorID ' + connectorID + ' transaction ' + connector.transactionId + ' value ' + connector.lastConsumptionValue); + const maxConsumption = self._stationInfo.maxPower * 3600 / interval; + logger.info(`${self._basicFormatLog()} MeterValues measurand ${sampledValueLcl.sampledValue[index].measurand ? sampledValueLcl.sampledValue[index].measurand : 'Energy.Active.Import.Register'}: connectorID ${connectorID}, transaction ${connector.transactionId}, value ${connector.lastConsumptionValue}`); sampledValueLcl.sampledValue[index].value = connector.lastConsumptionValue; - if (sampledValueLcl.sampledValue[index].value > (self._stationInfo.maxPower * 3600 / interval) || sampledValueLcl.sampledValue[index].value < 500) { - logger.info(self._basicFormatLog() + ' Meter type: ' + - (sampledValueLcl.sampledValue[index].measurand ? sampledValueLcl.sampledValue[index].measurand : 'default') + - ' value: ' + sampledValueLcl.sampledValue[index].value + '/' + (self._stationInfo.maxPower * 3600 / interval)); + if (sampledValueLcl.sampledValue[index].value > maxConsumption || debug) { + logger.error(`${self._basicFormatLog()} MeterValues measurand ${sampledValueLcl.sampledValue[index].measurand ? sampledValueLcl.sampledValue[index].measurand : 'Energy.Active.Import.Register'}: connectorID ${connectorID}, transaction ${connector.transactionId}, value: ${sampledValueLcl.sampledValue[index].value}/${maxConsumption}`); } } } @@ -655,42 +800,35 @@ class ChargingStation { transactionId: self._connectors[connectorID].transactionId, meterValue: [sampledValueLcl], }; - await self.sendMessage(uuid(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'MeterValues'); + await self.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'MeterValues'); } catch (error) { - logger.error(self._basicFormatLog() + ' Send meter values error: ' + error); + logger.error(self._basicFormatLog() + ' Send MeterValues error: ' + error); } } - async startMeterValues(connectorID, interval, self) { + async startMeterValues(connectorID, interval) { if (!this._connectors[connectorID].transactionStarted) { - logger.debug(`${self._basicFormatLog()} Trying to start meter values on connector ID ${connectorID} with no transaction started`); + logger.error(`${this._basicFormatLog()} Trying to start MeterValues on connector ID ${connectorID} with no transaction started`); + return; } else if (this._connectors[connectorID].transactionStarted && !this._connectors[connectorID].transactionId) { - logger.debug(`${self._basicFormatLog()} Trying to start meter values on connector ID ${connectorID} with no transaction id`); + logger.error(`${this._basicFormatLog()} Trying to start MeterValues on connector ID ${connectorID} with no transaction id`); + return; } - this._connectors[connectorID].transactionInterval = setInterval(async () => { + this._connectors[connectorID].transactionSetInterval = setInterval(async () => { const sendMeterValues = performance.timerify(this.sendMeterValues); this._performanceObserver.observe({ entryTypes: ['function'], }); - await sendMeterValues(connectorID, interval, self); + await sendMeterValues(connectorID, interval, this); }, interval); } - async handleRemoteStopTransaction(commandPayload) { - for (const connector in this._connectors) { - if (this._connectors[connector].transactionId === commandPayload.transactionId) { - this.sendStopTransaction(commandPayload.transactionId, connector); - } - } - return Constants.OCPP_RESPONSE_ACCEPTED; - } - hasAuthorizedTags() { - return Array.isArray(this._authorizedTags) && this._authorizedTags.length > 0; + return !Utils.isEmptyArray(this._authorizedTags); } getRandomTagId() { - const index = Math.round(Math.floor(Math.random() * this._authorizedTags.length - 1)); + const index = Math.floor(Math.random() * this._authorizedTags.length); return this._authorizedTags[index]; }