X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2FChargingStation.js;h=f5e1f460021394d7f427b21a99625a342f5aabcd;hb=fee830211b36e79b9d89d4c0d689d02bc6da4156;hp=b691b210bedc94d182e8b3393c319e9b2736b264;hpb=f4e4184e48126e45d8def8084910e05f46ca6af5;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStation.js b/src/charging-station/ChargingStation.js index b691b210..f5e1f460 100644 --- a/src/charging-station/ChargingStation.js +++ b/src/charging-station/ChargingStation.js @@ -1,18 +1,22 @@ -const Configuration = require('../utils/Configuration'); -const logger = require('../utils/Logger'); -const WebSocket = require('ws'); -const Constants = require('../utils/Constants'); -const Utils = require('../utils/Utils'); -const OCPPError = require('./OcppError'); -const AutomaticTransactionGenerator = require('./AutomaticTransactionGenerator'); -const Statistics = require('../utils/Statistics'); -const fs = require('fs'); -const {performance, PerformanceObserver} = require('perf_hooks'); - -class ChargingStation { +import {PerformanceObserver, performance} from 'perf_hooks'; + +import AutomaticTransactionGenerator from './AutomaticTransactionGenerator.js'; +import Configuration from '../utils/Configuration.js'; +import Constants from '../utils/Constants.js'; +import ElectricUtils from '../utils/ElectricUtils.js'; +import OCPPError from './OcppError.js'; +import Statistics from '../utils/Statistics.js'; +import Utils from '../utils/Utils.js'; +import WebSocket from 'ws'; +import crypto from 'crypto'; +import fs from 'fs'; +import logger from '../utils/Logger.js'; + +export default class ChargingStation { constructor(index, stationTemplateFile) { this._index = index; this._stationTemplateFile = stationTemplateFile; + this._connectors = {}; this._initialize(); this._isSocketRestart = false; @@ -38,7 +42,8 @@ class ChargingStation { stationTemplateFromFile = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8')); fs.closeSync(fileDescriptor); } catch (error) { - logger.error(this._basicFormatLog() + ' Template file loading error: ' + error); + logger.error('Template file ' + this._stationTemplateFile + ' loading error: ' + error); + throw error; } const stationTemplate = stationTemplateFromFile || {}; if (!Utils.isEmptyArray(stationTemplateFromFile.power)) { @@ -63,43 +68,65 @@ class ChargingStation { this._supervisionUrl = this._getSupervisionURL(); this._wsConnectionUrl = this._supervisionUrl + '/' + this._stationInfo.name; // Build connectors if needed - const maxConnectors = this._getMaxConnectors(); - const connectorsConfig = Utils.cloneJSonDocument(this._stationInfo.Connectors); - const connectorsConfigLength = Utils.convertToBoolean(this._stationInfo.useConnectorId0) && Object.keys(connectorsConfig).includes('0') ? Object.keys(connectorsConfig).length : Object.keys(connectorsConfig).length - 1; - if (!this._connectors || (this._connectors && Object.keys(this._connectors).length !== connectorsConfigLength)) { - this._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 && Utils.convertToBoolean(this._stationInfo.useConnectorId0) && - connectorsConfig[lastConnector]) { - this._connectors[lastConnector] = connectorsConfig[lastConnector]; + const maxConnectors = this._getMaxNumberOfConnectors(); + if (maxConnectors <= 0) { + logger.warn(`${this._logPrefix()} Charging station template ${this._stationTemplateFile} with ${maxConnectors} connectors`); + } + const templateMaxConnectors = this._getTemplateMaxNumberOfConnectors(); + if (templateMaxConnectors <= 0) { + logger.warn(`${this._logPrefix()} Charging station template ${this._stationTemplateFile} with no connector configurations`); + } + // Sanity check + if (maxConnectors > (this._stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) && !Utils.convertToBoolean(this._stationInfo.randomConnectors)) { + logger.warn(`${this._logPrefix()} Number of connectors exceeds the number of connector configurations in template ${this._stationTemplateFile}, forcing random connector configurations affectation`); + this._stationInfo.randomConnectors = true; + } + const connectorsConfigHash = crypto.createHash('sha256').update(JSON.stringify(this._stationInfo.Connectors) + maxConnectors.toString()).digest('hex'); + // FIXME: Handle shrinking the number of connectors + if (!this._connectors || (this._connectors && this._connectorsConfigurationHash !== connectorsConfigHash)) { + this._connectorsConfigurationHash = connectorsConfigHash; + // Add connector Id 0 + let lastConnector = 0; + for (lastConnector in this._stationInfo.Connectors) { + if (Utils.convertToInt(lastConnector) === 0 && Utils.convertToBoolean(this._stationInfo.useConnectorId0) && this._stationInfo.Connectors[lastConnector]) { + this._connectors[lastConnector] = Utils.cloneObject(this._stationInfo.Connectors[lastConnector]); } } - this._addConfigurationKey('NumberOfConnectors', maxConnectors, true); // Generate all connectors - for (let index = 1; index <= maxConnectors; index++) { - const randConnectorID = Utils.convertToBoolean(this._stationInfo.randomConnectors) ? Utils.getRandomInt(lastConnector, 1) : index; - this._connectors[index] = connectorsConfig[randConnectorID]; + if ((this._stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0) { + for (let index = 1; index <= maxConnectors; index++) { + const randConnectorID = Utils.convertToBoolean(this._stationInfo.randomConnectors) ? Utils.getRandomInt(lastConnector, 1) : index; + this._connectors[index] = Utils.cloneObject(this._stationInfo.Connectors[randConnectorID]); + } } } + // Avoid duplication of connectors related information + delete this._stationInfo.Connectors; // Initialize transaction attributes on connectors for (const connector in this._connectors) { - if (!this._connectors[connector].transactionStarted) { + if (!this.getConnector(connector).transactionStarted) { this._initTransactionOnConnector(connector); } } - this._statistics = new Statistics(this._stationInfo.name); - this._performanceObserver = new PerformanceObserver((list) => { - const entry = list.getEntries()[0]; - this._statistics.logPerformance(entry, 'ChargingStation'); - this._performanceObserver.disconnect(); - }); + // OCPP parameters + this._addConfigurationKey('NumberOfConnectors', this._getNumberOfConnectors(), true); + if (!this._getConfigurationKey('MeterValuesSampledData')) { + this._addConfigurationKey('MeterValuesSampledData', 'Energy.Active.Import.Register'); + } + this._stationInfo.powerDivider = this._getPowerDivider(); + if (this.getEnableStatistics()) { + this._statistics = Statistics.getInstance(); + this._statistics.objName = this._stationInfo.name; + this._performanceObserver = new PerformanceObserver((list) => { + const entry = list.getEntries()[0]; + this._statistics.logPerformance(entry, 'ChargingStation'); + this._performanceObserver.disconnect(); + }); + } } - _basicFormatLog() { - return Utils.basicFormatLog(` ${this._stationInfo.name}:`); + _logPrefix() { + return Utils.logPrefix(` ${this._stationInfo.name}:`); } _getConfiguration() { @@ -120,10 +147,11 @@ class ChargingStation { authorizedTags = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8')); fs.closeSync(fileDescriptor); } catch (error) { - logger.error(this._basicFormatLog() + ' Authorization file loading error: ' + error); + logger.error(this._logPrefix() + ' Authorization file ' + authorizationFile + ' loading error: ' + error); + throw error; } } else { - logger.info(this._basicFormatLog() + ' No authorization file given in template file ' + this._stationTemplateFile); + logger.info(this._logPrefix() + ' No authorization file given in template file ' + this._stationTemplateFile); } return authorizedTags; } @@ -137,23 +165,80 @@ class ChargingStation { return !Utils.isEmptyArray(this._authorizedTags); } - _getConnector(number) { - return this._stationInfo.Connectors[number]; + getEnableStatistics() { + return !Utils.isUndefined(this._stationInfo.enableStatistics) ? Utils.convertToBoolean(this._stationInfo.enableStatistics) : true; + } + + _getNumberOfPhases() { + return !Utils.isUndefined(this._stationInfo.numberOfPhases) ? Utils.convertToInt(this._stationInfo.numberOfPhases) : 3; + } + + _getNumberOfRunningTransactions() { + let trxCount = 0; + for (const connector in this._connectors) { + if (this.getConnector(connector).transactionStarted) { + trxCount++; + } + } + return trxCount; + } + + _getPowerDivider() { + let powerDivider = this._getNumberOfConnectors(); + if (this._stationInfo.powerSharedByConnectors) { + powerDivider = this._getNumberOfRunningTransactions(); + } + return powerDivider; + } + + getConnector(id) { + return this._connectors[Utils.convertToInt(id)]; } - _getMaxConnectors() { + _getTemplateMaxNumberOfConnectors() { + return Object.keys(this._stationInfo.Connectors).length; + } + + _getMaxNumberOfConnectors() { let maxConnectors = 0; if (!Utils.isEmptyArray(this._stationInfo.numberOfConnectors)) { - // Get evenly the number of connectors + // Distribute evenly the number of connectors maxConnectors = this._stationInfo.numberOfConnectors[(this._index - 1) % this._stationInfo.numberOfConnectors.length]; - } else { + } else if (!Utils.isUndefined(this._stationInfo.numberOfConnectors)) { maxConnectors = this._stationInfo.numberOfConnectors; + } else { + maxConnectors = this._stationInfo.Connectors[0] ? this._getTemplateMaxNumberOfConnectors() - 1 : this._getTemplateMaxNumberOfConnectors(); } return maxConnectors; } + _getNumberOfConnectors() { + return this._connectors[0] ? Object.keys(this._connectors).length - 1 : Object.keys(this._connectors).length; + } + + _getVoltageOut() { + const errMsg = `${this._logPrefix()} Unknown ${this._getPowerOutType()} powerOutType in template file ${this._stationTemplateFile}, cannot define default voltage out`; + let defaultVoltageOut; + switch (this._getPowerOutType()) { + case 'AC': + defaultVoltageOut = 230; + break; + case 'DC': + defaultVoltageOut = 400; + break; + default: + logger.error(errMsg); + throw Error(errMsg); + } + return !Utils.isUndefined(this._stationInfo.voltageOut) ? Utils.convertToInt(this._stationInfo.voltageOut) : defaultVoltageOut; + } + + _getPowerOutType() { + return !Utils.isUndefined(this._stationInfo.powerOutType) ? this._stationInfo.powerOutType : 'AC'; + } + _getSupervisionURL() { - const supervisionUrls = Utils.cloneJSonDocument(this._stationInfo.supervisionURL ? this._stationInfo.supervisionURL : Configuration.getSupervisionURLs()); + const supervisionUrls = Utils.cloneObject(this._stationInfo.supervisionURL ? this._stationInfo.supervisionURL : Configuration.getSupervisionURLs()); let indexUrl = 0; if (!Utils.isEmptyArray(supervisionUrls)) { if (Configuration.getDistributeStationToTenantEqually()) { @@ -182,9 +267,9 @@ class ChargingStation { this._startHeartbeat(this); // Initialize connectors status for (const connector in this._connectors) { - if (!this._connectors[connector].transactionStarted) { - if (this._connectors[connector].bootStatus) { - this.sendStatusNotificationWithTimeout(connector, this._connectors[connector].bootStatus); + if (!this.getConnector(connector).transactionStarted) { + if (this.getConnector(connector).bootStatus) { + this.sendStatusNotificationWithTimeout(connector, this.getConnector(connector).bootStatus); } else { this.sendStatusNotificationWithTimeout(connector, 'Available'); } @@ -201,7 +286,9 @@ class ChargingStation { this._automaticTransactionGeneration.start(); } } - this._statistics.start(); + if (this.getEnableStatistics()) { + this._statistics.start(); + } } // eslint-disable-next-line class-methods-use-this @@ -210,9 +297,9 @@ class ChargingStation { self._heartbeatSetInterval = setInterval(() => { this.sendHeartbeat(); }, self._heartbeatInterval); - logger.info(self._basicFormatLog() + ' Heartbeat started every ' + self._heartbeatInterval + 'ms'); + logger.info(self._logPrefix() + ' Heartbeat started every ' + self._heartbeatInterval + 'ms'); } else { - logger.error(`${self._basicFormatLog()} Heartbeat interval set to ${self._heartbeatInterval}, not starting the heartbeat`); + logger.error(`${self._logPrefix()} Heartbeat interval set to ${self._heartbeatInterval}ms, not starting the heartbeat`); } } @@ -227,11 +314,11 @@ class ChargingStation { // eslint-disable-next-line no-unused-vars fs.watchFile(this._getAuthorizationFile(), (current, previous) => { try { - logger.debug(this._basicFormatLog() + ' Authorization file ' + this._getAuthorizationFile() + ' have changed, reload'); + logger.debug(this._logPrefix() + ' Authorization file ' + this._getAuthorizationFile() + ' have changed, reload'); // Initialize _authorizedTags this._authorizedTags = this._loadAndGetAuthorizedTags(); } catch (error) { - logger.error(this._basicFormatLog() + ' Authorization file monitoring error: ' + error); + logger.error(this._logPrefix() + ' Authorization file monitoring error: ' + error); } }); } @@ -240,35 +327,39 @@ class ChargingStation { // eslint-disable-next-line no-unused-vars fs.watchFile(this._stationTemplateFile, (current, previous) => { try { - logger.debug(this._basicFormatLog() + ' Template file ' + this._stationTemplateFile + ' have changed, reload'); + logger.debug(this._logPrefix() + ' Template file ' + this._stationTemplateFile + ' have changed, reload'); // Initialize this._initialize(); this._addConfigurationKey('HeartBeatInterval', Utils.convertToInt(this._heartbeatInterval ? this._heartbeatInterval / 1000 : 0)); this._addConfigurationKey('HeartbeatInterval', Utils.convertToInt(this._heartbeatInterval ? this._heartbeatInterval / 1000 : 0), false, false); } catch (error) { - logger.error(this._basicFormatLog() + ' Charging station template file monitoring error: ' + error); + logger.error(this._logPrefix() + ' Charging station template file monitoring error: ' + error); } }); } - async _startMeterValues(connectorID, interval) { - if (!this._connectors[connectorID].transactionStarted) { - logger.error(`${this._basicFormatLog()} Trying to start MeterValues on connector ID ${connectorID} with no transaction started`); + async _startMeterValues(connectorId, interval) { + if (!this.getConnector(connectorId).transactionStarted) { + logger.error(`${this._logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction started`); return; - } else if (this._connectors[connectorID].transactionStarted && !this._connectors[connectorID].transactionId) { - logger.error(`${this._basicFormatLog()} Trying to start MeterValues on connector ID ${connectorID} with no transaction id`); + } else if (this.getConnector(connectorId).transactionStarted && !this.getConnector(connectorId).transactionId) { + logger.error(`${this._logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction id`); return; } if (interval > 0) { - this._connectors[connectorID].transactionSetInterval = setInterval(async () => { - const sendMeterValues = performance.timerify(this.sendMeterValues); - this._performanceObserver.observe({ - entryTypes: ['function'], - }); - await sendMeterValues(connectorID, interval, this); + this.getConnector(connectorId).transactionSetInterval = setInterval(async () => { + if (this.getEnableStatistics()) { + const sendMeterValues = performance.timerify(this.sendMeterValues); + this._performanceObserver.observe({ + entryTypes: ['function'], + }); + await sendMeterValues(connectorId, interval, this); + } else { + await this.sendMeterValues(connectorId, interval, this); + } }, interval); } else { - logger.error(`${this._basicFormatLog()} Charging station MeterValueSampleInterval configuration set to ${interval}ms, not sending MeterValues`); + logger.error(`${this._logPrefix()} Charging station MeterValueSampleInterval configuration set to ${interval}ms, not sending MeterValues`); } } @@ -277,7 +368,7 @@ class ChargingStation { this._wsConnectionUrl = this._supervisionUrl + '/' + this._stationInfo.name; } this._wsConnection = new WebSocket(this._wsConnectionUrl, 'ocpp' + Constants.OCPP_VERSION_16); - logger.info(this._basicFormatLog() + ' Will communicate with ' + this._supervisionUrl); + logger.info(this._logPrefix() + ' Will communicate through URL ' + this._supervisionUrl); // Monitor authorization file this._startAuthorizationFileMonitoring(); // Monitor station template file @@ -294,18 +385,18 @@ class ChargingStation { this._wsConnection.on('ping', this.onPing.bind(this)); } - async stop(type = '') { + async stop(reason = '') { // Stop heartbeat await this._stopHeartbeat(); // Stop the ATG if (Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.enable) && this._automaticTransactionGeneration && !this._automaticTransactionGeneration.timeToStop) { - await this._automaticTransactionGeneration.stop(type ? type + 'Reset' : ''); + await this._automaticTransactionGeneration.stop(reason); } else { for (const connector in this._connectors) { - if (this._connectors[connector].transactionStarted) { - await this.sendStopTransaction(this._connectors[connector].transactionId, type ? type + 'Reset' : ''); + if (this.getConnector(connector).transactionStarted) { + await this.sendStopTransaction(this.getConnector(connector).transactionId, reason); } } } @@ -319,7 +410,7 @@ class ChargingStation { } _reconnect(error) { - logger.error(this._basicFormatLog() + ' Socket: abnormally closed', error); + logger.error(this._logPrefix() + ' Socket: abnormally closed', error); // Stop the ATG if needed if (Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.enable) && Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.stopOnConnectionFailure) && @@ -331,19 +422,19 @@ class ChargingStation { this._stopHeartbeat(); if (this._autoReconnectTimeout !== 0 && (this._autoReconnectRetryCount < this._autoReconnectMaxRetries || this._autoReconnectMaxRetries === -1)) { - logger.error(`${this._basicFormatLog()} Socket: connection retry with timeout ${this._autoReconnectTimeout}ms`); + logger.error(`${this._logPrefix()} Socket: connection retry with timeout ${this._autoReconnectTimeout}ms`); this._autoReconnectRetryCount++; setTimeout(() => { - logger.error(this._basicFormatLog() + ' Socket: reconnecting try #' + this._autoReconnectRetryCount); + logger.error(this._logPrefix() + ' Socket: reconnecting try #' + this._autoReconnectRetryCount); this.start(); }, this._autoReconnectTimeout); } else if (this._autoReconnectTimeout !== 0 || this._autoReconnectMaxRetries !== -1) { - logger.error(`${this._basicFormatLog()} Socket: max retries reached (${this._autoReconnectRetryCount}) or retry disabled (${this._autoReconnectTimeout})`); + logger.error(`${this._logPrefix()} Socket: max retries reached (${this._autoReconnectRetryCount}) or retry disabled (${this._autoReconnectTimeout})`); } } onOpen() { - logger.info(`${this._basicFormatLog()} Is connected to server through ${this._wsConnectionUrl}`); + logger.info(`${this._logPrefix()} Is connected to server through ${this._wsConnectionUrl}`); if (!this._isSocketRestart) { // Send BootNotification this.sendBootNotification(); @@ -369,7 +460,7 @@ class ChargingStation { this._reconnect(error); break; default: - logger.error(this._basicFormatLog() + ' Socket error: ' + error); + logger.error(this._logPrefix() + ' Socket error: ' + error); break; } } @@ -378,7 +469,7 @@ class ChargingStation { switch (error) { case 1000: // Normal close case 1005: - logger.info(this._basicFormatLog() + ' Socket normally closed ' + error); + logger.info(this._logPrefix() + ' Socket normally closed ' + error); this._autoReconnectRetryCount = 0; break; default: // Abnormal close @@ -389,7 +480,7 @@ class ChargingStation { } onPing() { - logger.debug(this._basicFormatLog() + ' Has received a WS ping (rfc6455) from the server'); + logger.debug(this._logPrefix() + ' Has received a WS ping (rfc6455) from the server'); } async onMessage(message) { @@ -444,7 +535,7 @@ class ChargingStation { } } catch (error) { // Log - logger.error('%s Incoming message %j processing error %s on request content %s', this._basicFormatLog(), message, error, this._requests[messageId]); + logger.error('%s Incoming message %j processing error %s on request content %s', this._logPrefix(), message, error, this._requests[messageId]); // Send error // await this.sendError(messageId, error); } @@ -457,7 +548,7 @@ class ChargingStation { }; this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'Heartbeat'); } catch (error) { - logger.error(this._basicFormatLog() + ' Send Heartbeat error: ' + error); + logger.error(this._logPrefix() + ' Send Heartbeat error: ' + error); throw error; } } @@ -466,7 +557,7 @@ class ChargingStation { try { this.sendMessage(Utils.generateUUID(), this._bootNotificationMessage, Constants.OCPP_JSON_CALL_MESSAGE, 'BootNotification'); } catch (error) { - logger.error(this._basicFormatLog() + ' Send BootNotification error: ' + error); + logger.error(this._logPrefix() + ' Send BootNotification error: ' + error); throw error; } } @@ -480,7 +571,7 @@ class ChargingStation { }; await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StatusNotification'); } catch (error) { - logger.error(this._basicFormatLog() + ' Send StatusNotification error: ' + error); + logger.error(this._logPrefix() + ' Send StatusNotification error: ' + error); throw error; } } @@ -489,92 +580,265 @@ class ChargingStation { setTimeout(() => this.sendStatusNotification(connectorId, status, errorCode), timeout); } - async sendStartTransaction(connectorID, idTag) { + async sendStartTransaction(connectorId, idTag) { try { const payload = { - connectorId: connectorID, + connectorId, idTag, meterStart: 0, timestamp: new Date().toISOString(), }; return await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StartTransaction'); } catch (error) { - logger.error(this._basicFormatLog() + ' Send StartTransaction error: ' + error); + logger.error(this._logPrefix() + ' Send StartTransaction error: ' + error); throw error; } } - sendStartTransactionWithTimeout(connectorID, idTag, timeout) { - setTimeout(() => this.sendStartTransaction(connectorID, idTag), timeout); + sendStartTransactionWithTimeout(connectorId, idTag, timeout = Constants.START_TRANSACTION_TIMEOUT) { + setTimeout(() => this.sendStartTransaction(connectorId, idTag), timeout); } async sendStopTransaction(transactionId, reason = '') { try { - const payload = { - transactionId, - meterStop: 0, - timestamp: new Date().toISOString(), - reason, - }; + let payload; + if (reason) { + payload = { + transactionId, + meterStop: 0, + timestamp: new Date().toISOString(), + reason, + }; + } else { + payload = { + transactionId, + meterStop: 0, + timestamp: new Date().toISOString(), + }; + } await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StopTransaction'); } catch (error) { - logger.error(this._basicFormatLog() + ' Send StopTransaction error: ' + error); + logger.error(this._logPrefix() + ' Send StopTransaction error: ' + error); throw error; } } // eslint-disable-next-line class-methods-use-this - async sendMeterValues(connectorID, interval, self, debug = false) { + async sendMeterValues(connectorId, interval, self, debug = false) { try { - const sampledValueLcl = { + const sampledValues = { timestamp: new Date().toISOString(), + sampledValue: [], }; - const meterValuesClone = Utils.cloneJSonDocument(self._getConnector(connectorID).MeterValues); - if (!Utils.isEmptyArray(meterValuesClone)) { - sampledValueLcl.sampledValue = meterValuesClone; - } else { - sampledValueLcl.sampledValue = [meterValuesClone]; - } - for (let index = 0; index < sampledValueLcl.sampledValue.length; index++) { - const connector = self._connectors[connectorID]; + const meterValuesTemplate = self.getConnector(connectorId).MeterValues; + for (let index = 0; index < meterValuesTemplate.length; index++) { + const connector = self.getConnector(connectorId); // SoC measurand - if (sampledValueLcl.sampledValue[index].measurand && sampledValueLcl.sampledValue[index].measurand === 'SoC') { - 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}`); + if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === 'SoC' && self._getConfigurationKey('MeterValuesSampledData').value.includes('SoC')) { + sampledValues.sampledValue.push({ + ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? {unit: meterValuesTemplate[index].unit} : {unit: 'Percent'}, + ...!Utils.isUndefined(meterValuesTemplate[index].context) && {context: meterValuesTemplate[index].context}, + measurand: meterValuesTemplate[index].measurand, + ...!Utils.isUndefined(meterValuesTemplate[index].location) ? {location: meterValuesTemplate[index].location} : {location: 'EV'}, + ...!Utils.isUndefined(meterValuesTemplate[index].value) ? {value: meterValuesTemplate[index].value} : {value: Utils.getRandomInt(100)}, + }); + const sampledValuesIndex = sampledValues.sampledValue.length - 1; + if (sampledValues.sampledValue[sampledValuesIndex].value > 100 || debug) { + logger.error(`${self._logPrefix()} MeterValues measurand ${sampledValues.sampledValue[sampledValuesIndex].measurand ? sampledValues.sampledValue[sampledValuesIndex].measurand : 'Energy.Active.Import.Register'}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${sampledValues.sampledValue[sampledValuesIndex].value}/100`); } // Voltage measurand - } else if (sampledValueLcl.sampledValue[index].measurand && sampledValueLcl.sampledValue[index].measurand === 'Voltage') { - sampledValueLcl.sampledValue[index].value = 230; + } else if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === 'Voltage' && self._getConfigurationKey('MeterValuesSampledData').value.includes('Voltage')) { + sampledValues.sampledValue.push({ + ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? {unit: meterValuesTemplate[index].unit} : {unit: 'V'}, + ...!Utils.isUndefined(meterValuesTemplate[index].context) && {context: meterValuesTemplate[index].context}, + measurand: meterValuesTemplate[index].measurand, + ...!Utils.isUndefined(meterValuesTemplate[index].location) && {location: meterValuesTemplate[index].location}, + ...!Utils.isUndefined(meterValuesTemplate[index].value) ? {value: meterValuesTemplate[index].value} : {value: self._getVoltageOut()}, + }); + for (let phase = 1; self._getPowerOutType() === 'AC' && self._getNumberOfPhases() === 3 && phase <= self._getNumberOfPhases(); phase++) { + const voltageValue = sampledValues.sampledValue[sampledValues.sampledValue.length - 1].value; + let phaseValue; + if (voltageValue >= 0 && voltageValue <= 250) { + phaseValue = `L${phase}-N`; + } else if (voltageValue > 250) { + phaseValue = `L${phase}-L${(phase + 1) % self._getNumberOfPhases() !== 0 ? (phase + 1) % self._getNumberOfPhases() : self._getNumberOfPhases()}`; + } + sampledValues.sampledValue.push({ + ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? {unit: meterValuesTemplate[index].unit} : {unit: 'V'}, + ...!Utils.isUndefined(meterValuesTemplate[index].context) && {context: meterValuesTemplate[index].context}, + measurand: meterValuesTemplate[index].measurand, + ...!Utils.isUndefined(meterValuesTemplate[index].location) && {location: meterValuesTemplate[index].location}, + ...!Utils.isUndefined(meterValuesTemplate[index].value) ? {value: meterValuesTemplate[index].value} : {value: self._getVoltageOut()}, + phase: phaseValue, + }); + } + // Power.Active.Import measurand + } else if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === 'Power.Active.Import' && self._getConfigurationKey('MeterValuesSampledData').value.includes('Power.Active.Import')) { + // FIXME: factor out powerDivider checks + if (Utils.isUndefined(self._stationInfo.powerDivider)) { + const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'}: powerDivider is undefined`; + logger.error(errMsg); + throw Error(errMsg); + } else if (self._stationInfo.powerDivider && self._stationInfo.powerDivider <= 0) { + const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'}: powerDivider have zero or below value ${self._stationInfo.powerDivider}`; + logger.error(errMsg); + throw Error(errMsg); + } + const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'}: Unknown ${self._getPowerOutType()} powerOutType in template file ${self._stationTemplateFile}, cannot calculate ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'} measurand value`; + const powerMeasurandValues = {}; + const maxPower = Math.round(self._stationInfo.maxPower / self._stationInfo.powerDivider); + const maxPowerPerPhase = Math.round((self._stationInfo.maxPower / self._stationInfo.powerDivider) / self._getNumberOfPhases()); + switch (self._getPowerOutType()) { + case 'AC': + if (Utils.isUndefined(meterValuesTemplate[index].value)) { + powerMeasurandValues.L1 = Utils.getRandomFloatRounded(maxPowerPerPhase); + powerMeasurandValues.L2 = 0; + powerMeasurandValues.L3 = 0; + if (self._getNumberOfPhases() === 3) { + powerMeasurandValues.L2 = Utils.getRandomFloatRounded(maxPowerPerPhase); + powerMeasurandValues.L3 = Utils.getRandomFloatRounded(maxPowerPerPhase); + } + powerMeasurandValues.all = Utils.roundTo(powerMeasurandValues.L1 + powerMeasurandValues.L2 + powerMeasurandValues.L3, 2); + } + break; + case 'DC': + if (Utils.isUndefined(meterValuesTemplate[index].value)) { + powerMeasurandValues.all = Utils.getRandomFloatRounded(maxPower); + } + break; + default: + logger.error(errMsg); + throw Error(errMsg); + } + sampledValues.sampledValue.push({ + ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? {unit: meterValuesTemplate[index].unit} : {unit: 'W'}, + ...!Utils.isUndefined(meterValuesTemplate[index].context) && {context: meterValuesTemplate[index].context}, + measurand: meterValuesTemplate[index].measurand, + ...!Utils.isUndefined(meterValuesTemplate[index].location) && {location: meterValuesTemplate[index].location}, + ...!Utils.isUndefined(meterValuesTemplate[index].value) ? {value: meterValuesTemplate[index].value} : {value: powerMeasurandValues.all}, + }); + const sampledValuesIndex = sampledValues.sampledValue.length - 1; + if (sampledValues.sampledValue[sampledValuesIndex].value > maxPower || debug) { + logger.error(`${self._logPrefix()} MeterValues measurand ${sampledValues.sampledValue[sampledValuesIndex].measurand ? sampledValues.sampledValue[sampledValuesIndex].measurand : 'Energy.Active.Import.Register'}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${sampledValues.sampledValue[sampledValuesIndex].value}/${maxPower}`); + } + for (let phase = 1; self._getPowerOutType() === 'AC' && self._getNumberOfPhases() === 3 && phase <= self._getNumberOfPhases(); phase++) { + const phaseValue = `L${phase}-N`; + sampledValues.sampledValue.push({ + ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? {unit: meterValuesTemplate[index].unit} : {unit: 'W'}, + ...!Utils.isUndefined(meterValuesTemplate[index].context) && {context: meterValuesTemplate[index].context}, + ...!Utils.isUndefined(meterValuesTemplate[index].measurand) && {measurand: meterValuesTemplate[index].measurand}, + ...!Utils.isUndefined(meterValuesTemplate[index].location) && {location: meterValuesTemplate[index].location}, + ...!Utils.isUndefined(meterValuesTemplate[index].value) ? {value: meterValuesTemplate[index].value} : {value: powerMeasurandValues[`L${phase}`]}, + phase: phaseValue, + }); + } + // Current.Import measurand + } else if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === 'Current.Import' && self._getConfigurationKey('MeterValuesSampledData').value.includes('Current.Import')) { + // FIXME: factor out powerDivider checks + if (Utils.isUndefined(self._stationInfo.powerDivider)) { + const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'}: powerDivider is undefined`; + logger.error(errMsg); + throw Error(errMsg); + } else if (self._stationInfo.powerDivider && self._stationInfo.powerDivider <= 0) { + const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'}: powerDivider have zero or below value ${self._stationInfo.powerDivider}`; + logger.error(errMsg); + throw Error(errMsg); + } + const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'}: Unknown ${self._getPowerOutType()} powerOutType in template file ${self._stationTemplateFile}, cannot calculate ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'} measurand value`; + const currentMeasurandValues = {}; + let maxAmperage; + switch (self._getPowerOutType()) { + case 'AC': + maxAmperage = ElectricUtils.ampPerPhaseFromPower(self._getNumberOfPhases(), self._stationInfo.maxPower / self._stationInfo.powerDivider, self._getVoltageOut()); + if (Utils.isUndefined(meterValuesTemplate[index].value)) { + currentMeasurandValues.L1 = Utils.getRandomFloatRounded(maxAmperage); + currentMeasurandValues.L2 = 0; + currentMeasurandValues.L3 = 0; + if (self._getNumberOfPhases() === 3) { + currentMeasurandValues.L2 = Utils.getRandomFloatRounded(maxAmperage); + currentMeasurandValues.L3 = Utils.getRandomFloatRounded(maxAmperage); + } + currentMeasurandValues.all = Utils.roundTo((currentMeasurandValues.L1 + currentMeasurandValues.L2 + currentMeasurandValues.L3) / self._getNumberOfPhases(), 2); + } + break; + case 'DC': + maxAmperage = ElectricUtils.ampTotalFromPower(self._stationInfo.maxPower / self._stationInfo.powerDivider, self._getVoltageOut()); + if (Utils.isUndefined(meterValuesTemplate[index].value)) { + currentMeasurandValues.all = Utils.getRandomFloatRounded(maxAmperage); + } + break; + default: + logger.error(errMsg); + throw Error(errMsg); + } + sampledValues.sampledValue.push({ + ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? {unit: meterValuesTemplate[index].unit} : {unit: 'A'}, + ...!Utils.isUndefined(meterValuesTemplate[index].context) && {context: meterValuesTemplate[index].context}, + measurand: meterValuesTemplate[index].measurand, + ...!Utils.isUndefined(meterValuesTemplate[index].location) && {location: meterValuesTemplate[index].location}, + ...!Utils.isUndefined(meterValuesTemplate[index].value) ? {value: meterValuesTemplate[index].value} : {value: currentMeasurandValues.all}, + }); + const sampledValuesIndex = sampledValues.sampledValue.length - 1; + if (sampledValues.sampledValue[sampledValuesIndex].value > maxAmperage || debug) { + logger.error(`${self._logPrefix()} MeterValues measurand ${sampledValues.sampledValue[sampledValuesIndex].measurand ? sampledValues.sampledValue[sampledValuesIndex].measurand : 'Energy.Active.Import.Register'}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${sampledValues.sampledValue[sampledValuesIndex].value}/${maxAmperage}`); + } + for (let phase = 1; self._getPowerOutType() === 'AC' && self._getNumberOfPhases() === 3 && phase <= self._getNumberOfPhases(); phase++) { + const phaseValue = `L${phase}`; + sampledValues.sampledValue.push({ + ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? {unit: meterValuesTemplate[index].unit} : {unit: 'A'}, + ...!Utils.isUndefined(meterValuesTemplate[index].context) && {context: meterValuesTemplate[index].context}, + ...!Utils.isUndefined(meterValuesTemplate[index].measurand) && {measurand: meterValuesTemplate[index].measurand}, + ...!Utils.isUndefined(meterValuesTemplate[index].location) && {location: meterValuesTemplate[index].location}, + ...!Utils.isUndefined(meterValuesTemplate[index].value) ? {value: meterValuesTemplate[index].value} : {value: currentMeasurandValues[phaseValue]}, + phase: phaseValue, + }); + } // Energy.Active.Import.Register measurand (default) - } else if (!sampledValueLcl.sampledValue[index].measurand || sampledValueLcl.sampledValue[index].measurand === 'Energy.Active.Import.Register') { - // Persist previous value in connector - const consumption = Utils.getRandomInt(self._stationInfo.maxPower / 3600000 * interval); - if (connector && connector.lastConsumptionValue >= 0) { - connector.lastConsumptionValue += consumption; - } else { - connector.lastConsumptionValue = 0; + } else if (!meterValuesTemplate[index].measurand || meterValuesTemplate[index].measurand === 'Energy.Active.Import.Register') { + if (Utils.isUndefined(self._stationInfo.powerDivider)) { + const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'}: powerDivider is undefined`; + logger.error(errMsg); + throw Error(errMsg); + } else if (self._stationInfo.powerDivider && self._stationInfo.powerDivider <= 0) { + const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'}: powerDivider have zero or below value ${self._stationInfo.powerDivider}`; + logger.error(errMsg); + throw Error(errMsg); } - 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 > 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}`); + if (Utils.isUndefined(meterValuesTemplate[index].value)) { + const measurandValue = Utils.getRandomInt(self._stationInfo.maxPower / (self._stationInfo.powerDivider * 3600000) * interval); + // Persist previous value in connector + if (connector && !Utils.isNullOrUndefined(connector.lastEnergyActiveImportRegisterValue) && connector.lastEnergyActiveImportRegisterValue >= 0) { + connector.lastEnergyActiveImportRegisterValue += measurandValue; + } else { + connector.lastEnergyActiveImportRegisterValue = 0; + } + } + sampledValues.sampledValue.push({ + ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? {unit: meterValuesTemplate[index].unit} : {unit: 'Wh'}, + ...!Utils.isUndefined(meterValuesTemplate[index].context) && {context: meterValuesTemplate[index].context}, + ...!Utils.isUndefined(meterValuesTemplate[index].measurand) && {measurand: meterValuesTemplate[index].measurand}, + ...!Utils.isUndefined(meterValuesTemplate[index].location) && {location: meterValuesTemplate[index].location}, + ...!Utils.isUndefined(meterValuesTemplate[index].value) ? {value: meterValuesTemplate[index].value} : {value: connector.lastEnergyActiveImportRegisterValue}, + }); + const sampledValuesIndex = sampledValues.sampledValue.length - 1; + const maxConsumption = self._stationInfo.maxPower * 3600 / (self._stationInfo.powerDivider * interval); + if (sampledValues.sampledValue[sampledValuesIndex].value > maxConsumption || debug) { + logger.error(`${self._logPrefix()} MeterValues measurand ${sampledValues.sampledValue[sampledValuesIndex].measurand ? sampledValues.sampledValue[sampledValuesIndex].measurand : 'Energy.Active.Import.Register'}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${sampledValues.sampledValue[sampledValuesIndex].value}/${maxConsumption}`); } // Unsupported measurand } else { - logger.info(`${self._basicFormatLog()} Unsupported MeterValues measurand ${sampledValueLcl.sampledValue[index].measurand ? sampledValueLcl.sampledValue[index].measurand : 'Energy.Active.Import.Register'} on connectorID ${connectorID}`); + logger.info(`${self._logPrefix()} Unsupported MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : 'Energy.Active.Import.Register'} on connectorId ${connectorId}`); } } const payload = { - connectorId: connectorID, - transactionId: self._connectors[connectorID].transactionId, - meterValue: [sampledValueLcl], + connectorId, + transactionId: self.getConnector(connectorId).transactionId, + meterValue: sampledValues, }; await self.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'MeterValues'); } catch (error) { - logger.error(self._basicFormatLog() + ' Send MeterValues error: ' + error); + logger.error(self._logPrefix() + ' Send MeterValues error: ' + error); throw error; } } @@ -596,21 +860,27 @@ class ChargingStation { switch (messageType) { // Request case Constants.OCPP_JSON_CALL_MESSAGE: - this._statistics.addMessage(commandName); + if (this.getEnableStatistics()) { + this._statistics.addMessage(commandName); + } // Build request this._requests[messageId] = [responseCallback, rejectCallback, command]; messageToSend = JSON.stringify([messageType, messageId, commandName, command]); break; // Response case Constants.OCPP_JSON_CALL_RESULT_MESSAGE: - this._statistics.addMessage(commandName); + if (this.getEnableStatistics()) { + this._statistics.addMessage(commandName); + } // Build response messageToSend = JSON.stringify([messageType, messageId, command]); break; // Error Message case Constants.OCPP_JSON_CALL_ERROR_MESSAGE: + if (this.getEnableStatistics()) { + this._statistics.addMessage(`Error ${command.code ? command.code : Constants.OCPP_ERROR_GENERIC_ERROR} on ${commandName}`); + } // Build Message - 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; } @@ -634,20 +904,16 @@ class ChargingStation { // Function that will receive the request's response function responseCallback(payload, requestPayload) { - self._statistics.addMessage(commandName, true); - const responseCallbackFn = 'handleResponse' + commandName; - if (typeof self[responseCallbackFn] === 'function') { - self[responseCallbackFn](payload, requestPayload, self); - } else { - logger.debug(self._basicFormatLog() + ' Trying to call an undefined response callback function: ' + responseCallbackFn); - } + self.handleResponse(commandName, payload, requestPayload, self); // Send the response resolve(payload); } // 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); + if (self.getEnableStatistics()) { + 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 @@ -658,6 +924,19 @@ class ChargingStation { }); } + // eslint-disable-next-line class-methods-use-this + handleResponse(commandName, payload, requestPayload, self) { + if (self.getEnableStatistics()) { + self._statistics.addMessage(commandName, true); + } + const responseCallbackFn = 'handleResponse' + commandName; + if (typeof self[responseCallbackFn] === 'function') { + self[responseCallbackFn](payload, requestPayload, self); + } else { + logger.error(self._logPrefix() + ' Trying to call an undefined response callback function: ' + responseCallbackFn); + } + } + handleResponseBootNotification(payload) { if (payload.status === 'Accepted') { this._heartbeatInterval = payload.interval * 1000; @@ -665,29 +944,30 @@ class ChargingStation { this._addConfigurationKey('HeartbeatInterval', Utils.convertToInt(payload.interval), false, false); this._basicStartMessageSequence(); } else if (payload.status === 'Pending') { - logger.info(this._basicFormatLog() + ' Charging station pending on the central server'); + logger.info(this._logPrefix() + ' Charging station pending on the central server'); } else { - logger.info(this._basicFormatLog() + ' Charging station rejected by the central server'); + logger.info(this._logPrefix() + ' Charging station rejected by the central server'); } } - _initTransactionOnConnector(connectorID) { - this._connectors[connectorID].transactionStarted = false; - this._connectors[connectorID].transactionId = null; - this._connectors[connectorID].idTag = null; - this._connectors[connectorID].lastConsumptionValue = -1; + _initTransactionOnConnector(connectorId) { + this.getConnector(connectorId).transactionStarted = false; + this.getConnector(connectorId).transactionId = null; + this.getConnector(connectorId).idTag = null; + this.getConnector(connectorId).lastEnergyActiveImportRegisterValue = -1; } - _resetTransactionOnConnector(connectorID) { - this._initTransactionOnConnector(connectorID); - if (this._connectors[connectorID].transactionSetInterval) { - clearInterval(this._connectors[connectorID].transactionSetInterval); + _resetTransactionOnConnector(connectorId) { + this._initTransactionOnConnector(connectorId); + if (this.getConnector(connectorId).transactionSetInterval) { + clearInterval(this.getConnector(connectorId).transactionSetInterval); } } handleResponseStartTransaction(payload, requestPayload) { - 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); + if (this.getConnector(requestPayload.connectorId).transactionStarted) { + logger.debug(this._logPrefix() + ' Try to start a transaction on an already used connector ' + requestPayload.connectorId + ': %s', this.getConnector(requestPayload.connectorId)); + return; } let transactionConnectorId; @@ -698,22 +978,25 @@ class ChargingStation { } } if (!transactionConnectorId) { - logger.error(this._basicFormatLog() + ' Try to start a transaction on a non existing connector Id ' + requestPayload.connectorId); + logger.error(this._logPrefix() + ' 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.getConnector(requestPayload.connectorId).transactionStarted = true; + this.getConnector(requestPayload.connectorId).transactionId = payload.transactionId; + this.getConnector(requestPayload.connectorId).idTag = requestPayload.idTag; + this.getConnector(requestPayload.connectorId).lastEnergyActiveImportRegisterValue = 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); + logger.info(this._logPrefix() + ' Transaction ' + payload.transactionId + ' STARTED on ' + this._stationInfo.name + '#' + requestPayload.connectorId + ' for idTag ' + requestPayload.idTag); + if (this._stationInfo.powerSharedByConnectors) { + this._stationInfo.powerDivider++; + } 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); - this._resetTransactionOnConnector(transactionConnectorId); + logger.error(this._logPrefix() + ' Starting transaction id ' + payload.transactionId + ' REJECTED with status ' + payload.idTagInfo.status + ', idTag ' + requestPayload.idTag); + this._resetTransactionOnConnector(requestPayload.connectorId); this.sendStatusNotification(requestPayload.connectorId, 'Available'); } } @@ -721,47 +1004,52 @@ class ChargingStation { handleResponseStopTransaction(payload, requestPayload) { let transactionConnectorId; for (const connector in this._connectors) { - if (this._connectors[connector].transactionId === requestPayload.transactionId) { + if (this.getConnector(connector).transactionId === requestPayload.transactionId) { transactionConnectorId = connector; break; } } if (!transactionConnectorId) { - logger.error(this._basicFormatLog() + ' Try to stop a non existing transaction ' + requestPayload.transactionId); + logger.error(this._logPrefix() + ' 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); + if (this._stationInfo.powerSharedByConnectors) { + this._stationInfo.powerDivider--; + } + logger.info(this._logPrefix() + ' Transaction ' + requestPayload.transactionId + ' STOPPED on ' + this._stationInfo.name + '#' + transactionConnectorId); this._resetTransactionOnConnector(transactionConnectorId); } else { - logger.error(this._basicFormatLog() + ' Stopping transaction id ' + this._connectors[transactionConnectorId].transactionId + ' REJECTED with status ' + payload.idTagInfo.status); + logger.error(this._logPrefix() + ' Stopping transaction id ' + requestPayload.transactionId + ' REJECTED with status ' + payload.idTagInfo.status); } } handleResponseStatusNotification(payload, requestPayload) { - logger.debug(this._basicFormatLog() + ' Status notification response received: %j to StatusNotification request: %j', payload, requestPayload); + logger.debug(this._logPrefix() + ' Status notification response received: %j to StatusNotification request: %j', payload, requestPayload); } handleResponseMeterValues(payload, requestPayload) { - logger.debug(this._basicFormatLog() + ' MeterValues response received: %j to MeterValues request: %j', payload, requestPayload); + logger.debug(this._logPrefix() + ' 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); + logger.debug(this._logPrefix() + ' Heartbeat response received: %j to Heartbeat request: %j', payload, requestPayload); } async handleRequest(messageId, commandName, commandPayload) { - let result; - this._statistics.addMessage(commandName, true); + if (this.getEnableStatistics()) { + this._statistics.addMessage(commandName, true); + } + let response; // Call if (typeof this['handle' + commandName] === 'function') { try { - // Call the method - result = await this['handle' + commandName](commandPayload); + // Call the method to build the response + response = await this['handle' + commandName](commandPayload); } catch (error) { // Log - logger.error(this._basicFormatLog() + ' Handle request error: ' + error); + logger.error(this._logPrefix() + ' Handle request error: ' + error); // Send back response to inform backend await this.sendError(messageId, error); } @@ -771,17 +1059,17 @@ class ChargingStation { throw new Error(`${commandName} is not implemented ${JSON.stringify(commandPayload, null, ' ')}`); } // Send response - await this.sendMessage(messageId, result, Constants.OCPP_JSON_CALL_RESULT_MESSAGE); + await this.sendMessage(messageId, response, Constants.OCPP_JSON_CALL_RESULT_MESSAGE); } async handleReset(commandPayload) { // Simulate charging station restart setImmediate(async () => { - await this.stop(commandPayload.type); + await this.stop(commandPayload.type + 'Reset'); await Utils.sleep(this._stationInfo.resetTime); await this.start(); }); - logger.info(`${this._basicFormatLog()} ${commandPayload.type} reset command received, simulating it. The station will be back online in ${this._stationInfo.resetTime}ms`); + logger.info(`${this._logPrefix()} ${commandPayload.type} reset command received, simulating it. The station will be back online in ${this._stationInfo.resetTime}ms`); return Constants.OCPP_RESPONSE_ACCEPTED; } @@ -895,29 +1183,28 @@ class ChargingStation { // Check if authorized if (this._authorizedTags.find((value) => value === commandPayload.idTag)) { // Authorization successful start transaction - 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); + this.sendStartTransactionWithTimeout(transactionConnectorID, commandPayload.idTag); + logger.debug(this._logPrefix() + ' 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); + logger.error(this._logPrefix() + ' Remote starting transaction REJECTED with status ' + commandPayload.idTagInfo.status + ', idTag ' + commandPayload.idTag); return Constants.OCPP_RESPONSE_REJECTED; } // No local authorization check required => start transaction - 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); + this.sendStartTransactionWithTimeout(transactionConnectorID, commandPayload.idTag); + logger.debug(this._logPrefix() + ' 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) { + if (this.getConnector(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); + logger.info(this._logPrefix() + ' Try to stop remotely a non existing transaction ' + commandPayload.transactionId); return Constants.OCPP_RESPONSE_REJECTED; } } -module.exports = ChargingStation;