From 5ad8570f1c504fb6893cc4ad5b656a09c2660136 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Tue, 20 Oct 2020 09:25:38 +0200 Subject: [PATCH 1/1] Add Reset command handling. MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit --- .../AutomaticTransactionGenerator.js | 22 +- src/charging-station/ChargingStation.js | 600 ++++++++++-------- 2 files changed, 333 insertions(+), 289 deletions(-) diff --git a/src/charging-station/AutomaticTransactionGenerator.js b/src/charging-station/AutomaticTransactionGenerator.js index ebb0a8e0..bb2119b2 100644 --- a/src/charging-station/AutomaticTransactionGenerator.js +++ b/src/charging-station/AutomaticTransactionGenerator.js @@ -24,17 +24,6 @@ class AutomaticTransactionGenerator { return Utils.basicFormatLog(' ' + this._chargingStation._stationInfo.name + ' ATG:'); } - async stop() { - logger.info(this._basicFormatLog() + ' ATG OVER => STOPPING ALL TRANSACTIONS'); - for (const connector in this._chargingStation._connectors) { - if (this._chargingStation._connectors[connector].transactionStarted) { - logger.info(this._basicFormatLog(connector) + ' ATG OVER. Stop transaction ' + this._chargingStation._connectors[connector].transactionId); - await this._chargingStation.sendStopTransaction(this._chargingStation._connectors[connector].transactionId); - } - } - this._timeToStop = true; - } - async start() { this._timeToStop = false; if (this._chargingStation._stationInfo.AutomaticTransactionGenerator.stopAfterHours && @@ -51,6 +40,17 @@ class AutomaticTransactionGenerator { } } + async stop() { + logger.info(this._basicFormatLog() + ' ATG OVER => STOPPING ALL TRANSACTIONS'); + for (const connector in this._chargingStation._connectors) { + if (this._chargingStation._connectors[connector].transactionStarted) { + logger.info(this._basicFormatLog(connector) + ' ATG OVER. Stop transaction ' + this._chargingStation._connectors[connector].transactionId); + await this._chargingStation.sendStopTransaction(this._chargingStation._connectors[connector].transactionId); + } + } + this._timeToStop = true; + } + async startConnector(connectorId) { do { const wait = Utils.getRandomInt(this._chargingStation._stationInfo.AutomaticTransactionGenerator.maxDelayBetweenTwoTransactions, diff --git a/src/charging-station/ChargingStation.js b/src/charging-station/ChargingStation.js index 1f5c9b45..d65be7a7 100644 --- a/src/charging-station/ChargingStation.js +++ b/src/charging-station/ChargingStation.js @@ -27,6 +27,30 @@ class ChargingStation { this._authorizedTags = this._loadAndGetAuthorizedTags(); } + _getStationName(stationTemplate) { + return stationTemplate.fixedName ? stationTemplate.baseName : stationTemplate.baseName + '-' + ('000000000' + this._index).substr(('000000000' + this._index).length - 4); + } + + _buildStationInfo() { + let stationTemplateFromFile; + try { + // Load template file + const fileDescriptor = fs.openSync(this._stationTemplateFile, 'r'); + stationTemplateFromFile = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8')); + fs.closeSync(fileDescriptor); + } catch (error) { + logger.error(this._basicFormatLog() + ' Template file loading error: ' + error); + } + const stationTemplate = stationTemplateFromFile || {}; + if (Array.isArray(stationTemplateFromFile.power)) { + stationTemplate.maxPower = stationTemplateFromFile.power[Math.floor(Math.random() * stationTemplateFromFile.power.length)]; + } else { + stationTemplate.maxPower = stationTemplateFromFile.power; + } + stationTemplate.name = this._getStationName(stationTemplateFromFile); + return stationTemplate; + } + _initialize() { this._stationInfo = this._buildStationInfo(); this._bootNotificationMessage = { @@ -37,6 +61,7 @@ class ChargingStation { }; this._configuration = this._getConfiguration(); this._supervisionUrl = this._getSupervisionURL(); + this._connectionUrl = this._supervisionUrl + '/' + this._stationInfo.name; this._statistics = new Statistics(this._stationInfo.name); this._performanceObserver = new PerformanceObserver((list) => { const entry = list.getEntries()[0]; @@ -75,33 +100,28 @@ class ChargingStation { return authorizedTags; } - _startAuthorizationFileMonitoring() { - // 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'); - // Initialize _authorizedTags - this._authorizedTags = this._loadAndGetAuthorizedTags(); - } catch (error) { - logger.error(this._basicFormatLog() + ' Authorization file monitoring error: ' + error); - } - }); + getRandomTagId() { + const index = Math.floor(Math.random() * this._authorizedTags.length); + return this._authorizedTags[index]; } - _startStationTemplateFileMonitoring() { - // 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'); - // 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); - this._addConfigurationKey('NumberOfConnectors', this._getMaxConnectors(), true); - } catch (error) { - logger.error(this._basicFormatLog() + ' Charging station template file monitoring error: ' + error); - } - }); + hasAuthorizedTags() { + return !Utils.isEmptyArray(this._authorizedTags); + } + + _getConnector(number) { + return this._stationInfo.Connectors[number]; + } + + _getMaxConnectors() { + let maxConnectors = 0; + if (Array.isArray(this._stationInfo.numberOfConnectors)) { + // Generate some connectors + maxConnectors = this._stationInfo.numberOfConnectors[(this._index - 1) % this._stationInfo.numberOfConnectors.length]; + } else { + maxConnectors = this._stationInfo.numberOfConnectors; + } + return maxConnectors; } _getSupervisionURL() { @@ -119,10 +139,6 @@ class ChargingStation { return supervisionUrls; } - _getStationName(stationTemplate) { - return stationTemplate.fixedName ? stationTemplate.baseName : stationTemplate.baseName + '-' + ('000000000' + this._index).substr(('000000000' + this._index).length - 4); - } - _getAuthorizeRemoteTxRequests() { const authorizeRemoteTxRequests = this._getConfigurationKey('AuthorizeRemoteTxRequests'); return authorizeRemoteTxRequests ? Utils.convertToBoolean(authorizeRemoteTxRequests.value) : false; @@ -133,29 +149,124 @@ class ChargingStation { return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false; } - _buildStationInfo() { - let stationTemplateFromFile; - try { - // Load template file - const fileDescriptor = fs.openSync(this._stationTemplateFile, 'r'); - stationTemplateFromFile = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8')); - fs.closeSync(fileDescriptor); - } catch (error) { - logger.error(this._basicFormatLog() + ' Template file loading error: ' + error); + async _basicStartMessageSequence() { + // Start heartbeat + this._startHeartbeat(this); + // Build connectors + if (!this._connectors) { + this._connectors = {}; + const connectorsConfig = Utils.cloneJSonDocument(this._stationInfo.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._getMaxConnectors(); + 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]; + } } - const stationTemplate = stationTemplateFromFile || {}; - if (Array.isArray(stationTemplateFromFile.power)) { - stationTemplate.maxPower = stationTemplateFromFile.power[Math.floor(Math.random() * stationTemplateFromFile.power.length)]; + + for (const connector in this._connectors) { + if (!this._connectors[connector].transactionStarted) { + if (this._connectors[connector].bootStatus) { + this.sendStatusNotificationWithTimeout(connector, this._connectors[connector].bootStatus); + } else { + this.sendStatusNotificationWithTimeout(connector, 'Available'); + } + } else { + this.sendStatusNotificationWithTimeout(connector, 'Charging'); + } + } + + if (Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.enable)) { + if (!this._automaticTransactionGeneration) { + this._automaticTransactionGeneration = new AutomaticTransactionGenerator(this); + } + if (this._automaticTransactionGeneration.timeToStop) { + this._automaticTransactionGeneration.start(); + } + } + this._statistics.start(); + } + + // eslint-disable-next-line class-methods-use-this + async _startHeartbeat(self) { + if (self._heartbeatInterval && !self._heartbeatSetInterval) { + self._heartbeatSetInterval = setInterval(() => { + try { + const payload = { + currentTime: new Date().toISOString(), + }; + self.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'Heartbeat'); + } catch (error) { + logger.error(self._basicFormatLog() + ' Send heartbeat error: ' + error); + } + }, self._heartbeatInterval); + logger.info(self._basicFormatLog() + ' Heartbeat started every ' + self._heartbeatInterval + 'ms'); } else { - stationTemplate.maxPower = stationTemplateFromFile.power; + logger.error(self._basicFormatLog() + ' Heartbeat interval undefined, not starting the heartbeat'); } - stationTemplate.name = this._getStationName(stationTemplateFromFile); - return stationTemplate; + } + + _startAuthorizationFileMonitoring() { + // 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'); + // Initialize _authorizedTags + this._authorizedTags = this._loadAndGetAuthorizedTags(); + } catch (error) { + logger.error(this._basicFormatLog() + ' Authorization file monitoring error: ' + error); + } + }); + } + + _startStationTemplateFileMonitoring() { + // 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'); + // 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); + this._addConfigurationKey('NumberOfConnectors', this._getMaxConnectors(), true); + } catch (error) { + logger.error(this._basicFormatLog() + ' 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`); + 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`); + return; + } + this._connectors[connectorID].transactionSetInterval = setInterval(async () => { + const sendMeterValues = performance.timerify(this.sendMeterValues); + this._performanceObserver.observe({ + entryTypes: ['function'], + }); + await sendMeterValues(connectorID, interval, this); + }, interval); } async start() { - this._url = this._supervisionUrl + '/' + this._stationInfo.name; - this._wsConnection = new WebSocket(this._url, 'ocpp1.6'); + if (!this._connectionUrl) { + this._connectionUrl = this._supervisionUrl + '/' + this._stationInfo.name; + } + this._wsConnection = new WebSocket(this._connectionUrl, 'ocpp1.6'); logger.info(this._basicFormatLog() + ' Will communicate with ' + this._supervisionUrl); // Monitor authorization file this._startAuthorizationFileMonitoring(); @@ -173,9 +284,63 @@ class ChargingStation { this._wsConnection.on('ping', this.onPing.bind(this)); } + async stop(type = '') { + // Stop heartbeat + if (this._heartbeatSetInterval) { + await clearInterval(this._heartbeatSetInterval); + this._heartbeatSetInterval = null; + } + // Stop the ATG + if (Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.enable) && + this._automaticTransactionGeneration && + !this._automaticTransactionGeneration.timeToStop) { + await this._automaticTransactionGeneration.stop(); + } else { + for (const connector in this._connectors) { + if (this._connectors[connector].transactionStarted) { + await this.sendStopTransaction(this._connectors[connector].transactionId, type ? type + 'Reset' : ''); + } + } + } + // eslint-disable-next-line guard-for-in + for (const connector in this._connectors) { + await this.sendStatusNotification(connector, 'Unavailable'); + } + if (this._wsConnection) { + await this._wsConnection.close(); + } + } + + _reconnect(error) { + logger.error(this._basicFormatLog() + ' Socket: abnormally closed', error); + // Stop the ATG if needed + if (Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.enable) && + Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.stopOnConnectionFailure) && + this._automaticTransactionGeneration && + !this._automaticTransactionGeneration.timeToStop) { + this._automaticTransactionGeneration.stop(); + } + // Stop heartbeat + if (this._heartbeatSetInterval) { + clearInterval(this._heartbeatSetInterval); + this._heartbeatSetInterval = null; + } + if (this._autoReconnectTimeout !== 0 && + (this._autoReconnectRetryCount < this._autoReconnectMaxRetries || this._autoReconnectMaxRetries === -1)) { + logger.error(`${this._basicFormatLog()} Socket: connection retry with timeout ${this._autoReconnectTimeout}ms`); + this._autoReconnectRetryCount++; + setTimeout(() => { + logger.error(this._basicFormatLog() + ' 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})`); + } + } + onOpen() { - logger.info(`${this._basicFormatLog()} Is connected to server through ${this._url}`); - if (!this._heartbeatInterval) { + logger.info(`${this._basicFormatLog()} Is connected to server through ${this._connectionUrl}`); + if (!this._isSocketRestart) { // Send BootNotification try { this.sendMessage(Utils.generateUUID(), this._bootNotificationMessage, Constants.OCPP_JSON_CALL_MESSAGE, 'BootNotification'); @@ -285,74 +450,110 @@ class ChargingStation { } } - // eslint-disable-next-line class-methods-use-this - async _startHeartbeat(self) { - if (self._heartbeatInterval && !self._heartbeatSetInterval) { - self._heartbeatSetInterval = setInterval(() => { - try { - const payload = { - currentTime: new Date().toISOString(), - }; - self.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'Heartbeat'); - } catch (error) { - logger.error(self._basicFormatLog() + ' Send heartbeat error: ' + error); - } - }, self._heartbeatInterval); - logger.info(self._basicFormatLog() + ' Heartbeat started every ' + self._heartbeatInterval + 'ms'); - } else { - logger.error(self._basicFormatLog() + ' Heartbeat interval undefined, not starting the heartbeat'); + async sendStatusNotification(connectorId, status, errorCode = 'NoError') { + try { + const payload = { + connectorId, + errorCode, + status, + }; + await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StatusNotification'); + } catch (error) { + logger.error(this._basicFormatLog() + ' Send status error: ' + error); + throw error; } } - _reconnect(error) { - logger.error(this._basicFormatLog() + ' Socket: abnormally closed', error); - // Stop heartbeat - if (this._heartbeatSetInterval) { - clearInterval(this._heartbeatSetInterval); - this._heartbeatSetInterval = null; - } - // Stop the ATG if needed - if (Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.enable) && - Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.stopOnConnectionFailure) && - this._automaticTransactionGeneration && - !this._automaticTransactionGeneration.timeToStop) { - this._automaticTransactionGeneration.stop(); - } - if (this._autoReconnectTimeout !== 0 && - (this._autoReconnectRetryCount < this._autoReconnectMaxRetries || this._autoReconnectMaxRetries === -1)) { - logger.error(`${this._basicFormatLog()} Socket: connection retry with timeout ${this._autoReconnectTimeout}ms`); - this._autoReconnectRetryCount++; - setTimeout(() => { - logger.error(this._basicFormatLog() + ' 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})`); + sendStatusNotificationWithTimeout(connectorId, status, errorCode = 'NoError', timeout = Constants.STATUS_NOTIFICATION_TIMEOUT) { + setTimeout(() => this.sendStatusNotification(connectorId, status, errorCode), timeout); + } + + async sendStartTransaction(connectorID, idTag) { + try { + const payload = { + 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 start transaction error: ' + error); + throw error; } } - sendError(messageId, err) { - // Check exception: only OCPP error are accepted - const error = err instanceof OCPPError ? err : new OCPPError(Constants.OCPP_ERROR_INTERNAL_ERROR, err.message); - // Send error - return this.sendMessage(messageId, error, Constants.OCPP_JSON_CALL_ERROR_MESSAGE); + sendStartTransactionWithTimeout(connectorID, idTag, timeout) { + setTimeout(() => this.sendStartTransaction(connectorID, idTag), timeout); } - async sendStatusNotification(connectorId, status, errorCode = 'NoError') { + async sendStopTransaction(transactionId, reason = '') { try { const payload = { - connectorId, - errorCode, - status, + transactionId, + meterStop: 0, + timestamp: new Date().toISOString(), + reason, }; - await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StatusNotification'); + await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StopTransaction'); } catch (error) { - logger.error(this._basicFormatLog() + ' Send status error: ' + error); + logger.error(this._basicFormatLog() + ' Send stop transaction error: ' + error); + throw error; } } - async sendStatusNotificationWithTimeout(connectorId, status, errorCode = 'NoError', timeout = Constants.STATUS_NOTIFICATION_TIMEOUT) { - setTimeout(() => this.sendStatusNotification(connectorId, status, errorCode), timeout); + // eslint-disable-next-line class-methods-use-this + async sendMeterValues(connectorID, interval, self, debug = false) { + try { + const sampledValueLcl = { + timestamp: new Date().toISOString(), + }; + const meterValuesClone = Utils.cloneJSonDocument(self._getConnector(connectorID).MeterValues); + if (Array.isArray(meterValuesClone)) { + sampledValueLcl.sampledValue = meterValuesClone; + } else { + sampledValueLcl.sampledValue = [meterValuesClone]; + } + for (let index = 0; index < sampledValueLcl.sampledValue.length; index++) { + const connector = self._connectors[connectorID]; + if (sampledValueLcl.sampledValue[index].measurand && sampledValueLcl.sampledValue[index].measurand === 'SoC') { + sampledValueLcl.sampledValue[index].value = Utils.getRandomInt(100); + if (sampledValueLcl.sampledValue[index].value > 100 || debug) { + logger.error(`${self._basicFormatLog()} MeterValues measurand ${sampledValueLcl.sampledValue[index].measurand ? sampledValueLcl.sampledValue[index].measurand : 'Energy.Active.Import.Register'}: connectorID ${connectorID}, transaction ${connector.transactionId}, value: ${sampledValueLcl.sampledValue[index].value}`); + } + } else { + // Persist previous value in connector + const consumption = Utils.getRandomInt(self._stationInfo.maxPower / 3600000 * interval); + if (connector && connector.lastConsumptionValue >= 0) { + connector.lastConsumptionValue += consumption; + } else { + connector.lastConsumptionValue = 0; + } + 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}`); + } + } + } + + const payload = { + connectorId: connectorID, + transactionId: self._connectors[connectorID].transactionId, + meterValue: [sampledValueLcl], + }; + await self.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'MeterValues'); + } catch (error) { + logger.error(self._basicFormatLog() + ' Send MeterValues error: ' + error); + } + } + + sendError(messageId, err) { + // Check exception: only OCPP error are accepted + const error = err instanceof OCPPError ? err : new OCPPError(Constants.OCPP_ERROR_INTERNAL_ERROR, err.message); + // Send error + return this.sendMessage(messageId, error, Constants.OCPP_JSON_CALL_ERROR_MESSAGE); } sendMessage(messageId, command, messageType = Constants.OCPP_JSON_CALL_RESULT_MESSAGE, commandName = '') { @@ -427,52 +628,15 @@ class ChargingStation { }); } - async _basicStartMessageSequence() { - // Start heartbeat - this._startHeartbeat(this); - // Build connectors - if (!this._connectors) { - this._connectors = {}; - const connectorsConfig = Utils.cloneJSonDocument(this._stationInfo.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._getMaxConnectors(); - 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]; - } - } - - for (const connector in this._connectors) { - if (!this._connectors[connector].transactionStarted) { - if (this._connectors[connector].bootStatus) { - this.sendStatusNotificationWithTimeout(connector, this._connectors[connector].bootStatus); - } else { - this.sendStatusNotificationWithTimeout(connector, 'Available'); - } - } else { - this.sendStatusNotificationWithTimeout(connector, 'Charging'); - } - } - - if (Utils.convertToBoolean(this._stationInfo.AutomaticTransactionGenerator.enable)) { - if (!this._automaticTransactionGeneration) { - this._automaticTransactionGeneration = new AutomaticTransactionGenerator(this); - } - if (this._automaticTransactionGeneration.timeToStop) { - this._automaticTransactionGeneration.start(); - } + handleResponseBootNotification(payload) { + if (payload.status === 'Accepted') { + this._heartbeatInterval = payload.interval * 1000; + this._addConfigurationKey('HeartBeatInterval', Utils.convertToInt(payload.interval)); + this._addConfigurationKey('HeartbeatInterval', Utils.convertToInt(payload.interval), false, false); + this._basicStartMessageSequence(); + } else { + logger.info(this._basicFormatLog() + ' Boot Notification rejected'); } - this._statistics.start(); } _resetTransactionOnConnector(connectorID) { @@ -486,17 +650,6 @@ class ChargingStation { } } - handleResponseBootNotification(payload) { - if (payload.status === 'Accepted') { - this._heartbeatInterval = payload.interval * 1000; - this._addConfigurationKey('HeartBeatInterval', Utils.convertToInt(payload.interval)); - this._addConfigurationKey('HeartbeatInterval', Utils.convertToInt(payload.interval), false, false); - this._basicStartMessageSequence(); - } else { - logger.info(this._basicFormatLog() + ' Boot Notification rejected'); - } - } - handleResponseStartTransaction(payload, requestPayload) { 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); @@ -522,7 +675,7 @@ class ChargingStation { this.sendStatusNotification(requestPayload.connectorId, 'Charging'); logger.info(this._basicFormatLog() + ' Transaction ' + this._connectors[transactionConnectorId].transactionId + ' STARTED on ' + this._stationInfo.name + '#' + requestPayload.connectorId + ' for idTag ' + requestPayload.idTag); const configuredMeterValueSampleInterval = this._getConfigurationKey('MeterValueSampleInterval'); - this.startMeterValues(requestPayload.connectorId, + 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); @@ -587,6 +740,17 @@ class ChargingStation { await this.sendMessage(messageId, result, Constants.OCPP_JSON_CALL_RESULT_MESSAGE); } + async handleReset(commandPayload) { + logger.info(`${this._basicFormatLog()} ${commandPayload.type} reset command received, simulating it`); + // Simulate charging station restart + setImmediate(async () => { + await this.stop(commandPayload.type); + await Utils.sleep(60000); + await this.start(); + }); + return Constants.OCPP_RESPONSE_ACCEPTED; + } + _getConfigurationKey(key) { return this._configuration.configurationKey.find((configElement) => configElement.key === key); } @@ -723,126 +887,6 @@ class ChargingStation { logger.info(this._basicFormatLog() + ' Try to stop remotely a non existing transaction ' + commandPayload.transactionId); return Constants.OCPP_RESPONSE_REJECTED; } - - async sendStartTransaction(connectorID, idTag) { - try { - const payload = { - 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 start transaction error: ' + error); - throw error; - } - } - async sendStartTransactionWithTimeout(connectorID, idTag, timeout) { - setTimeout(() => this.sendStartTransaction(connectorID, idTag), timeout); - } - - async sendStopTransaction(transactionId) { - try { - const 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 stop transaction error: ' + error); - throw error; - } - } - - // eslint-disable-next-line class-methods-use-this - async sendMeterValues(connectorID, interval, self, debug = false) { - try { - const sampledValueLcl = { - timestamp: new Date().toISOString(), - }; - const meterValuesClone = Utils.cloneJSonDocument(self._getConnector(connectorID).MeterValues); - if (Array.isArray(meterValuesClone)) { - sampledValueLcl.sampledValue = meterValuesClone; - } else { - sampledValueLcl.sampledValue = [meterValuesClone]; - } - for (let index = 0; index < sampledValueLcl.sampledValue.length; index++) { - const connector = self._connectors[connectorID]; - if (sampledValueLcl.sampledValue[index].measurand && sampledValueLcl.sampledValue[index].measurand === 'SoC') { - sampledValueLcl.sampledValue[index].value = Utils.getRandomInt(100); - if (sampledValueLcl.sampledValue[index].value > 100 || debug) { - logger.error(`${self._basicFormatLog()} MeterValues measurand ${sampledValueLcl.sampledValue[index].measurand ? sampledValueLcl.sampledValue[index].measurand : 'Energy.Active.Import.Register'}: connectorID ${connectorID}, transaction ${connector.transactionId}, value: ${sampledValueLcl.sampledValue[index].value}`); - } - } else { - // Persist previous value in connector - const consumption = Utils.getRandomInt(self._stationInfo.maxPower / 3600000 * interval); - if (connector && connector.lastConsumptionValue >= 0) { - connector.lastConsumptionValue += consumption; - } else { - connector.lastConsumptionValue = 0; - } - 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}`); - } - } - } - - const payload = { - connectorId: connectorID, - transactionId: self._connectors[connectorID].transactionId, - meterValue: [sampledValueLcl], - }; - await self.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'MeterValues'); - } catch (error) { - logger.error(self._basicFormatLog() + ' Send MeterValues 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`); - 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`); - return; - } - this._connectors[connectorID].transactionSetInterval = setInterval(async () => { - const sendMeterValues = performance.timerify(this.sendMeterValues); - this._performanceObserver.observe({ - entryTypes: ['function'], - }); - await sendMeterValues(connectorID, interval, this); - }, interval); - } - - hasAuthorizedTags() { - return !Utils.isEmptyArray(this._authorizedTags); - } - - getRandomTagId() { - const index = Math.floor(Math.random() * this._authorizedTags.length); - return this._authorizedTags[index]; - } - - _getConnector(number) { - return this._stationInfo.Connectors[number]; - } - - _getMaxConnectors() { - let maxConnectors = 0; - if (Array.isArray(this._stationInfo.numberOfConnectors)) { - // Generate some connectors - maxConnectors = this._stationInfo.numberOfConnectors[(this._index - 1) % this._stationInfo.numberOfConnectors.length]; - } else { - maxConnectors = this._stationInfo.numberOfConnectors; - } - return maxConnectors; - } } module.exports = ChargingStation; -- 2.34.1