X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;ds=sidebyside;f=src%2Fcharging-station%2FChargingStation.js;h=1ffcfab72f9923001770364de10166e9eb632df9;hb=5933cbc8c16288268032886a14e03d47b8ce84f6;hp=304a4e7abd6af4efd13bfd7fd9f28f9d906b4aca;hpb=488fd3a755df336223b6d469a07c7605d325289b;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStation.js b/src/charging-station/ChargingStation.js index 304a4e7a..1ffcfab7 100644 --- a/src/charging-station/ChargingStation.js +++ b/src/charging-station/ChargingStation.js @@ -1,16 +1,18 @@ -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 crypto = require('crypto'); -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; @@ -59,8 +61,8 @@ 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 : '', + ...!Utils.isUndefined(this._stationInfo.chargePointSerialNumberPrefix) && {chargePointSerialNumber: this._stationInfo.chargePointSerialNumberPrefix}, + ...!Utils.isUndefined(this._stationInfo.firmwareVersion) && {firmwareVersion: this._stationInfo.firmwareVersion}, }; this._configuration = this._getConfiguration(); this._supervisionUrl = this._getSupervisionURL(); @@ -68,36 +70,34 @@ class ChargingStation { // Build connectors if needed const maxConnectors = this._getMaxNumberOfConnectors(); if (maxConnectors <= 0) { - const errMsg = `${this._logPrefix()} Charging station template ${this._stationTemplateFile} with no connectors`; - logger.error(errMsg); - throw Error(errMsg); + 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; - // Determine number of customized connectors - let lastConnector; + // Add connector Id 0 + let lastConnector = 0; for (lastConnector in this._stationInfo.Connectors) { - // Add connector Id 0 - if (Utils.convertToBoolean(this._stationInfo.useConnectorId0) && this._stationInfo.Connectors[lastConnector] && - lastConnector === 0) { - this._connectors[lastConnector] = Utils.cloneJSonDocument(this._stationInfo.Connectors[lastConnector]); + 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); - if (!this._getConfigurationKey('MeterValuesSampledData')) { - this._addConfigurationKey('MeterValuesSampledData', 'Energy.Active.Import.Register'); - } - // Sanity check - if (maxConnectors > lastConnector && !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; - } // 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] = Utils.cloneJSonDocument(this._stationInfo.Connectors[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 @@ -108,6 +108,11 @@ class ChargingStation { this._initTransactionOnConnector(connector); } } + // 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(); @@ -161,7 +166,11 @@ class ChargingStation { } getEnableStatistics() { - return !Utils.isUndefined(this._stationInfo.enableStatistics) ? this._stationInfo.enableStatistics : true; + 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() { @@ -186,26 +195,50 @@ class ChargingStation { return this._connectors[Utils.convertToInt(id)]; } + _getTemplateMaxNumberOfConnectors() { + return Object.keys(this._stationInfo.Connectors).length; + } + _getMaxNumberOfConnectors() { let maxConnectors = 0; if (!Utils.isEmptyArray(this._stationInfo.numberOfConnectors)) { // Distribute evenly the number of connectors maxConnectors = this._stationInfo.numberOfConnectors[(this._index - 1) % this._stationInfo.numberOfConnectors.length]; - } else if (this._stationInfo.numberOfConnectors) { + } else if (!Utils.isUndefined(this._stationInfo.numberOfConnectors)) { maxConnectors = this._stationInfo.numberOfConnectors; } else { - maxConnectors = Utils.convertToBoolean(this._stationInfo.useConnectorId0) ? Object.keys(this._stationInfo.Connectors).length - 1 : - Object.keys(this._stationInfo.Connectors).length; + maxConnectors = this._stationInfo.Connectors[0] ? this._getTemplateMaxNumberOfConnectors() - 1 : this._getTemplateMaxNumberOfConnectors(); } return maxConnectors; } _getNumberOfConnectors() { - return Utils.convertToBoolean(this._stationInfo.useConnectorId0) ? Object.keys(this._connectors).length - 1 : Object.keys(this._connectors).length; + 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()) { @@ -266,7 +299,7 @@ class ChargingStation { }, self._heartbeatInterval); logger.info(self._logPrefix() + ' Heartbeat started every ' + self._heartbeatInterval + 'ms'); } else { - logger.error(`${self._logPrefix()} 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`); } } @@ -471,7 +504,7 @@ class ChargingStation { if (Utils.isIterable(this._requests[messageId])) { [responseCallback, , requestPayload] = this._requests[messageId]; } else { - throw new Error(`Response request for unknown message id ${messageId} is not iterable`); + throw new Error(`Response request for message id ${messageId} is not iterable`); } if (!responseCallback) { // Error @@ -491,7 +524,7 @@ class ChargingStation { if (Utils.isIterable(this._requests[messageId])) { [, rejectCallback] = this._requests[messageId]; } else { - throw new Error(`Error request for unknown message id ${messageId} is not iterable`); + throw new Error(`Error request for message id ${messageId} is not iterable`); } delete this._requests[messageId]; rejectCallback(new OCPPError(commandName, commandPayload, errorDetails)); @@ -562,27 +595,18 @@ class ChargingStation { } } - sendStartTransactionWithTimeout(connectorId, idTag, timeout) { + sendStartTransactionWithTimeout(connectorId, idTag, timeout = Constants.START_TRANSACTION_TIMEOUT) { setTimeout(() => this.sendStartTransaction(connectorId, idTag), timeout); } async sendStopTransaction(transactionId, reason = '') { try { - let payload; - if (reason) { - payload = { - transactionId, - meterStop: 0, - timestamp: new Date().toISOString(), - reason, - }; - } else { - payload = { - transactionId, - meterStop: 0, - timestamp: new Date().toISOString(), - }; - } + const payload = { + transactionId, + meterStop: 0, + timestamp: new Date().toISOString(), + ...reason && {reason}, + }; await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StopTransaction'); } catch (error) { logger.error(this._logPrefix() + ' Send StopTransaction error: ' + error); @@ -593,64 +617,216 @@ class ChargingStation { // eslint-disable-next-line class-methods-use-this 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 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' && self._getConfigurationKey('MeterValuesSampledData').value.includes('SoC')) { - sampledValueLcl.sampledValue[index].value = !Utils.isUndefined(sampledValueLcl.sampledValue[index].value) ? - sampledValueLcl.sampledValue[index].value : - sampledValueLcl.sampledValue[index].value = Utils.getRandomInt(100); - if (sampledValueLcl.sampledValue[index].value > 100 || debug) { - logger.error(`${self._logPrefix()} 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' && self._getConfigurationKey('MeterValuesSampledData').value.includes('Voltage')) { - sampledValueLcl.sampledValue[index].value = !Utils.isUndefined(sampledValueLcl.sampledValue[index].value) ? 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') { + } else if (!meterValuesTemplate[index].measurand || meterValuesTemplate[index].measurand === 'Energy.Active.Import.Register') { + // FIXME: factor out powerDivider checks if (Utils.isUndefined(self._stationInfo.powerDivider)) { - const errMsg = `${self._logPrefix()} MeterValues measurand ${sampledValueLcl.sampledValue[index].measurand ? sampledValueLcl.sampledValue[index].measurand : 'Energy.Active.Import.Register'}: powerDivider is undefined`; + 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 ${sampledValueLcl.sampledValue[index].measurand ? sampledValueLcl.sampledValue[index].measurand : 'Energy.Active.Import.Register'}: powerDivider have zero or below value ${self._stationInfo.powerDivider}`; + 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); } - if (Utils.isUndefined(sampledValueLcl.sampledValue[index].value)) { + 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 && connector.lastEnergyActiveImportRegisterValue >= 0) { + if (connector && !Utils.isNullOrUndefined(connector.lastEnergyActiveImportRegisterValue) && connector.lastEnergyActiveImportRegisterValue >= 0) { connector.lastEnergyActiveImportRegisterValue += measurandValue; } else { connector.lastEnergyActiveImportRegisterValue = 0; } - sampledValueLcl.sampledValue[index].value = connector.lastEnergyActiveImportRegisterValue; } - logger.info(`${self._logPrefix()} MeterValues measurand ${sampledValueLcl.sampledValue[index].measurand ? sampledValueLcl.sampledValue[index].measurand : 'Energy.Active.Import.Register'}: connectorId ${connectorId}, transaction ${connector.transactionId}, value ${sampledValueLcl.sampledValue[index].value}`); - const maxConsumption = self._stationInfo.maxPower * 3600 / (self._stationInfo.powerDivider * interval); - if (sampledValueLcl.sampledValue[index].value > maxConsumption || debug) { - logger.error(`${self._logPrefix()} 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}`); + 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 = Math.round(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._logPrefix()} 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, transactionId: self.getConnector(connectorId).transactionId, - meterValue: [sampledValueLcl], + meterValue: sampledValues, }; await self.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'MeterValues'); } catch (error) { @@ -715,39 +891,49 @@ class ChargingStation { } else if (this._wsConnection && this._wsConnection.readyState === WebSocket.OPEN) { // Send timeout in case connection is open otherwise wait for ever // FIXME: Handle message on timeout - setTimeout(() => rejectCallback(`Timeout for message ${messageId}`), Constants.OCPP_SOCKET_TIMEOUT); + setTimeout(() => rejectCallback(new OCPPError(command.code ? command.code : Constants.OCPP_ERROR_GENERIC_ERROR, command.message ? command.message : '', command.details ? command.details : {})), Constants.OCPP_SOCKET_TIMEOUT); } // Function that will receive the request's response function responseCallback(payload, requestPayload) { - if (self.getEnableStatistics()) { - self._statistics.addMessage(commandName, true); - } - const responseCallbackFn = 'handleResponse' + commandName; - if (typeof self[responseCallbackFn] === 'function') { - self[responseCallbackFn](payload, requestPayload, self); - } else { - logger.debug(self._logPrefix() + ' 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) { + function rejectCallback(error) { + if (!(error instanceof OCPPError)) { + const errMsg = `${self._logPrefix()} Argument error is not an instance of OCPPError in rejectCallback call`; + logger.error(errMsg); + throw Error(errMsg); + } + logger.debug(`${self._logPrefix()} Error %j on commandName %s command %j`, error, commandName, command); if (self.getEnableStatistics()) { - self._statistics.addMessage(`Error ${command.code ? command.code : Constants.OCPP_ERROR_GENERIC_ERROR} on ${commandName}`, true); + self._statistics.addMessage(`Error on commandName ${commandName}`, true); } // Build Exception // eslint-disable-next-line no-empty-function self._requests[messageId] = [() => { }, () => { }, '']; // Properly format the request - const error = reason instanceof OCPPError ? reason : new Error(reason); // Send error reject(error); } }); } + // 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; @@ -852,12 +1038,12 @@ class ChargingStation { if (this.getEnableStatistics()) { this._statistics.addMessage(commandName, true); } - let result; + 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._logPrefix() + ' Handle request error: ' + error); @@ -870,7 +1056,7 @@ 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) { @@ -994,7 +1180,7 @@ 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); + 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; } @@ -1002,7 +1188,7 @@ class ChargingStation { return Constants.OCPP_RESPONSE_REJECTED; } // No local authorization check required => start transaction - this.sendStartTransactionWithTimeout(transactionConnectorID, commandPayload.idTag, Constants.START_TRANSACTION_TIMEOUT); + 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; } @@ -1019,4 +1205,3 @@ class ChargingStation { } } -module.exports = ChargingStation;