X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2FChargingStation.js;h=304a4e7abd6af4efd13bfd7fd9f28f9d906b4aca;hb=488fd3a755df336223b6d469a07c7605d325289b;hp=c6b3a5ae517caf3a1a034d09d10a4f38b16127fa;hpb=13b4eba03d3d9e7e868dfeb41951735c58eedf32;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStation.js b/src/charging-station/ChargingStation.js index c6b3a5ae..304a4e7a 100644 --- a/src/charging-station/ChargingStation.js +++ b/src/charging-station/ChargingStation.js @@ -14,6 +14,7 @@ class ChargingStation { constructor(index, stationTemplateFile) { this._index = index; this._stationTemplateFile = stationTemplateFile; + this._connectors = {}; this._initialize(); this._isSocketRestart = false; @@ -39,7 +40,8 @@ class ChargingStation { stationTemplateFromFile = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8')); fs.closeSync(fileDescriptor); } catch (error) { - logger.error(this._logPrefix() + ' Template file loading error: ' + error); + logger.error('Template file ' + this._stationTemplateFile + ' loading error: ' + error); + throw error; } const stationTemplate = stationTemplateFromFile || {}; if (!Utils.isEmptyArray(stationTemplateFromFile.power)) { @@ -64,41 +66,58 @@ 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 connectorsConfigHash = crypto.createHash('sha256').update(JSON.stringify(connectorsConfig) + maxConnectors.toString()).digest('hex'); + 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); + } + 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; - 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]; + 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]); } } 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] = connectorsConfig[randConnectorID]; + this._connectors[index] = Utils.cloneJSonDocument(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); } } - // FIXME: Conditionally initialize or use singleton design pattern per charging station - 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(); - }); + 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(); + }); + } } _logPrefix() { @@ -123,7 +142,8 @@ class ChargingStation { authorizedTags = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8')); fs.closeSync(fileDescriptor); } catch (error) { - logger.error(this._logPrefix() + ' Authorization file loading error: ' + error); + logger.error(this._logPrefix() + ' Authorization file ' + authorizationFile + ' loading error: ' + error); + throw error; } } else { logger.info(this._logPrefix() + ' No authorization file given in template file ' + this._stationTemplateFile); @@ -140,21 +160,50 @@ class ChargingStation { return !Utils.isEmptyArray(this._authorizedTags); } - _getConnector(number) { - return this._stationInfo.Connectors[number]; + getEnableStatistics() { + return !Utils.isUndefined(this._stationInfo.enableStatistics) ? this._stationInfo.enableStatistics : true; + } + + _getNumberOfRunningTransactions() { + let trxCount = 0; + for (const connector in this._connectors) { + if (this.getConnector(connector).transactionStarted) { + trxCount++; + } + } + return trxCount; } - _getMaxConnectors() { + _getPowerDivider() { + let powerDivider = this._getNumberOfConnectors(); + if (this._stationInfo.powerSharedByConnectors) { + powerDivider = this._getNumberOfRunningTransactions(); + } + return powerDivider; + } + + getConnector(id) { + return this._connectors[Utils.convertToInt(id)]; + } + + _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 (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; } return maxConnectors; } + _getNumberOfConnectors() { + return Utils.convertToBoolean(this._stationInfo.useConnectorId0) ? Object.keys(this._connectors).length - 1 : Object.keys(this._connectors).length; + } + _getSupervisionURL() { const supervisionUrls = Utils.cloneJSonDocument(this._stationInfo.supervisionURL ? this._stationInfo.supervisionURL : Configuration.getSupervisionURLs()); let indexUrl = 0; @@ -185,9 +234,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'); } @@ -204,7 +253,9 @@ class ChargingStation { this._automaticTransactionGeneration.start(); } } - this._statistics.start(); + if (this.getEnableStatistics()) { + this._statistics.start(); + } } // eslint-disable-next-line class-methods-use-this @@ -255,20 +306,24 @@ class ChargingStation { } async _startMeterValues(connectorId, interval) { - if (!this._connectors[connectorId].transactionStarted) { - logger.error(`${this._logPrefix()} Trying to start MeterValues on connector ID ${connectorId} with no transaction started`); + 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._logPrefix()} 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._logPrefix()} Charging station MeterValueSampleInterval configuration set to ${interval}ms, not sending MeterValues`); @@ -307,8 +362,8 @@ class ChargingStation { await this._automaticTransactionGeneration.stop(reason); } else { for (const connector in this._connectors) { - if (this._connectors[connector].transactionStarted) { - await this.sendStopTransaction(this._connectors[connector].transactionId, reason); + if (this.getConnector(connector).transactionStarted) { + await this.sendStopTransaction(this.getConnector(connector).transactionId, reason); } } } @@ -541,16 +596,16 @@ class ChargingStation { const sampledValueLcl = { timestamp: new Date().toISOString(), }; - const meterValuesClone = Utils.cloneJSonDocument(self._getConnector(connectorId).MeterValues); + 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 connector = self.getConnector(connectorId); // SoC measurand - if (sampledValueLcl.sampledValue[index].measurand && sampledValueLcl.sampledValue[index].measurand === 'SoC') { + 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); @@ -558,12 +613,21 @@ class ChargingStation { 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}`); } // Voltage measurand - } else if (sampledValueLcl.sampledValue[index].measurand && sampledValueLcl.sampledValue[index].measurand === 'Voltage') { + } 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; // Energy.Active.Import.Register measurand (default) } else if (!sampledValueLcl.sampledValue[index].measurand || sampledValueLcl.sampledValue[index].measurand === 'Energy.Active.Import.Register') { + 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`; + 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}`; + logger.error(errMsg); + throw Error(errMsg); + } if (Utils.isUndefined(sampledValueLcl.sampledValue[index].value)) { - const measurandValue = Utils.getRandomInt(self._stationInfo.maxPower / 3600000 * interval); + const measurandValue = Utils.getRandomInt(self._stationInfo.maxPower / (self._stationInfo.powerDivider * 3600000) * interval); // Persist previous value in connector if (connector && connector.lastEnergyActiveImportRegisterValue >= 0) { connector.lastEnergyActiveImportRegisterValue += measurandValue; @@ -573,7 +637,7 @@ class ChargingStation { 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 / interval; + 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}`); } @@ -585,7 +649,7 @@ class ChargingStation { const payload = { connectorId, - transactionId: self._connectors[connectorId].transactionId, + transactionId: self.getConnector(connectorId).transactionId, meterValue: [sampledValueLcl], }; await self.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'MeterValues'); @@ -612,21 +676,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; } @@ -650,7 +720,9 @@ class ChargingStation { // Function that will receive the request's response function responseCallback(payload, requestPayload) { - self._statistics.addMessage(commandName, true); + if (self.getEnableStatistics()) { + self._statistics.addMessage(commandName, true); + } const responseCallbackFn = 'handleResponse' + commandName; if (typeof self[responseCallbackFn] === 'function') { self[responseCallbackFn](payload, requestPayload, self); @@ -663,7 +735,9 @@ class ChargingStation { // Function that will receive the request's rejection function rejectCallback(reason) { - self._statistics.addMessage(`Error ${command.code ? command.code : Constants.OCPP_ERROR_GENERIC_ERROR} on ${commandName || ''}`, true); + 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 @@ -688,22 +762,22 @@ class ChargingStation { } _initTransactionOnConnector(connectorId) { - this._connectors[connectorId].transactionStarted = false; - this._connectors[connectorId].transactionId = null; - this._connectors[connectorId].idTag = null; - this._connectors[connectorId].lastEnergyActiveImportRegisterValue = -1; + 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); + if (this.getConnector(connectorId).transactionSetInterval) { + clearInterval(this.getConnector(connectorId).transactionSetInterval); } } handleResponseStartTransaction(payload, requestPayload) { - if (this._connectors[requestPayload.connectorId].transactionStarted) { - logger.debug(this._logPrefix() + ' Try to start a transaction on an already used connector ' + requestPayload.connectorId + ': %s', this._connectors[requestPayload.connectorId]); + 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; } @@ -719,18 +793,21 @@ class ChargingStation { 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].lastEnergyActiveImportRegisterValue = 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._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._logPrefix() + ' Starting transaction id ' + payload.transactionId + ' REJECTED with status ' + payload.idTagInfo.status + ', idTag ' + requestPayload.idTag); - this._resetTransactionOnConnector(transactionConnectorId); + this._resetTransactionOnConnector(requestPayload.connectorId); this.sendStatusNotification(requestPayload.connectorId, 'Available'); } } @@ -738,7 +815,7 @@ 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; } @@ -749,6 +826,9 @@ class ChargingStation { } if (payload.idTagInfo && payload.idTagInfo.status === 'Accepted') { this.sendStatusNotification(transactionConnectorId, 'Available'); + if (this._stationInfo.powerSharedByConnectors) { + this._stationInfo.powerDivider--; + } logger.info(this._logPrefix() + ' Transaction ' + requestPayload.transactionId + ' STOPPED on ' + this._stationInfo.name + '#' + transactionConnectorId); this._resetTransactionOnConnector(transactionConnectorId); } else { @@ -769,8 +849,10 @@ class ChargingStation { } async handleRequest(messageId, commandName, commandPayload) { + if (this.getEnableStatistics()) { + this._statistics.addMessage(commandName, true); + } let result; - this._statistics.addMessage(commandName, true); // Call if (typeof this['handle' + commandName] === 'function') { try { @@ -927,7 +1009,7 @@ class ChargingStation { 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; }