X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2FChargingStation.ts;h=1bf867cb304b0361a0e872b61c3b8ee9adfdf717;hb=3574dfd3808a5894699e5fe246f723ebfee93de9;hp=843093e27fd226b17bbba8f7062b271a8808e5a6;hpb=9ac86a7ea04eb1155c2a720b36f6bf33ba336767;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 843093e2..1bf867cb 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -1,23 +1,24 @@ -import { AuthorizationStatus, StartTransactionResponse, StopTransactionReason, StopTransactionResponse } from '../types/Transaction'; +import { AuthorizationStatus, StartTransactionResponse, StopTransactionReason, StopTransactionResponse } from '../types/ocpp/1.6/Transaction'; import ChargingStationConfiguration, { ConfigurationKey } from '../types/ChargingStationConfiguration'; import ChargingStationTemplate, { PowerOutType } from '../types/ChargingStationTemplate'; -import { ConfigurationResponse, DefaultRequestResponse, UnlockResponse } from '../types/RequestResponses'; +import { ConfigurationResponse, DefaultRequestResponse, UnlockResponse } from '../types/ocpp/1.6/RequestResponses'; import Connectors, { Connector } from '../types/Connectors'; -import MeterValue, { MeterValueLocation, MeterValueMeasurand, MeterValuePhase, MeterValueUnit } from '../types/MeterValue'; +import MeterValue, { MeterValueLocation, MeterValueMeasurand, MeterValuePhase, MeterValueUnit } from '../types/ocpp/1.6/MeterValue'; import { PerformanceObserver, performance } from 'perf_hooks'; +import WebSocket, { MessageEvent } from 'ws'; import AutomaticTransactionGenerator from './AutomaticTransactionGenerator'; -import { ChargePointErrorCode } from '../types/ChargePointErrorCode'; -import { ChargePointStatus } from '../types/ChargePointStatus'; +import { ChargePointErrorCode } from '../types/ocpp/1.6/ChargePointErrorCode'; +import { ChargePointStatus } from '../types/ocpp/1.6/ChargePointStatus'; import ChargingStationInfo from '../types/ChargingStationInfo'; import Configuration from '../utils/Configuration'; -import Constants from '../utils/Constants.js'; +import Constants from '../utils/Constants'; import ElectricUtils from '../utils/ElectricUtils'; import MeasurandValues from '../types/MeasurandValues'; -import OCPPError from './OcppError.js'; +import OCPPError from './OcppError'; +import Requests from '../types/ocpp/1.6/Requests'; import Statistics from '../utils/Statistics'; import Utils from '../utils/Utils'; -import WebSocket from 'ws'; import crypto from 'crypto'; import fs from 'fs'; import logger from '../utils/Logger'; @@ -41,32 +42,31 @@ export default class ChargingStation { private _wsConnection: WebSocket; private _hasStopped: boolean; private _hasSocketRestarted: boolean; + private _connectionTimeout: number; private _autoReconnectRetryCount: number; private _autoReconnectMaxRetries: number; - private _autoReconnectTimeout: number; - private _requests: { [id: string]: [(payload?, requestPayload?) => void, (error?: OCPPError) => void, object] }; - private _messageQueue: any[]; + private _requests: Requests; + private _messageQueue: string[]; private _automaticTransactionGeneration: AutomaticTransactionGenerator; private _authorizedTags: string[]; private _heartbeatInterval: number; private _heartbeatSetInterval: NodeJS.Timeout; + private _webSocketPingSetInterval: NodeJS.Timeout; private _statistics: Statistics; private _performanceObserver: PerformanceObserver; constructor(index: number, stationTemplateFile: string) { this._index = index; this._stationTemplateFile = stationTemplateFile; - this._connectors = {}; + this._connectors = {} as Connectors; this._initialize(); this._hasStopped = false; this._hasSocketRestarted = false; this._autoReconnectRetryCount = 0; - this._autoReconnectMaxRetries = Configuration.getAutoReconnectMaxRetries(); // -1 for unlimited - this._autoReconnectTimeout = Configuration.getAutoReconnectTimeout() * 1000; // Ms, zero for disabling - this._requests = {}; - this._messageQueue = []; + this._requests = {} as Requests; + this._messageQueue = [] as string[]; this._authorizedTags = this._loadAndGetAuthorizedTags(); } @@ -83,7 +83,7 @@ export default class ChargingStation { stationTemplateFromFile = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8')) as ChargingStationTemplate; fs.closeSync(fileDescriptor); } catch (error) { - logger.error('Template file ' + this._stationTemplateFile + ' loading error: ' + error); + logger.error('Template file ' + this._stationTemplateFile + ' loading error: %j', error); throw error; } const stationInfo: ChargingStationInfo = stationTemplateFromFile || {} as ChargingStationInfo; @@ -110,9 +110,11 @@ export default class ChargingStation { ...!Utils.isUndefined(this._stationInfo.chargeBoxSerialNumberPrefix) && { chargeBoxSerialNumber: this._stationInfo.chargeBoxSerialNumberPrefix }, ...!Utils.isUndefined(this._stationInfo.firmwareVersion) && { firmwareVersion: this._stationInfo.firmwareVersion }, }; - this._configuration = this._getConfiguration(); + this._configuration = this._getTemplateChargingStationConfiguration(); this._supervisionUrl = this._getSupervisionURL(); this._wsConnectionUrl = this._supervisionUrl + '/' + this._stationInfo.name; + this._connectionTimeout = this._getConnectionTimeout() * 1000; // Ms, zero for disabling + this._autoReconnectMaxRetries = this._getAutoReconnectMaxRetries(); // -1 for unlimited // Build connectors if needed const maxConnectors = this._getMaxNumberOfConnectors(); if (maxConnectors <= 0) { @@ -183,7 +185,7 @@ export default class ChargingStation { return Utils.logPrefix(` ${this._stationInfo.name}:`); } - _getConfiguration(): ChargingStationConfiguration { + _getTemplateChargingStationConfiguration(): ChargingStationConfiguration { return this._stationInfo.Configuration ? this._stationInfo.Configuration : {} as ChargingStationConfiguration; } @@ -201,7 +203,7 @@ export default class ChargingStation { authorizedTags = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8')) as string[]; fs.closeSync(fileDescriptor); } catch (error) { - logger.error(this._logPrefix() + ' Authorization file ' + authorizationFile + ' loading error: ' + error); + logger.error(this._logPrefix() + ' Authorization file ' + authorizationFile + ' loading error: %j', error); throw error; } } else { @@ -242,6 +244,26 @@ export default class ChargingStation { return trxCount; } + _getConnectionTimeout(): number { + if (!Utils.isUndefined(this._stationInfo.connectionTimeout)) { + return this._stationInfo.connectionTimeout; + } + if (!Utils.isUndefined(Configuration.getConnectionTimeout())) { + return Configuration.getConnectionTimeout(); + } + return 30; + } + + _getAutoReconnectMaxRetries(): number { + if (!Utils.isUndefined(this._stationInfo.autoReconnectMaxRetries)) { + return this._stationInfo.autoReconnectMaxRetries; + } + if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) { + return Configuration.getAutoReconnectMaxRetries(); + } + return -1; + } + _getPowerDivider(): number { let powerDivider = this._getNumberOfConnectors(); if (this._stationInfo.powerSharedByConnectors) { @@ -263,7 +285,7 @@ export default class ChargingStation { if (!Utils.isEmptyArray(this._stationInfo.numberOfConnectors)) { const numberOfConnectors = this._stationInfo.numberOfConnectors as number[]; // Distribute evenly the number of connectors - maxConnectors = numberOfConnectors[(this._index - 1) % numberOfConnectors.length] ; + maxConnectors = numberOfConnectors[(this._index - 1) % numberOfConnectors.length]; } else if (!Utils.isUndefined(this._stationInfo.numberOfConnectors)) { maxConnectors = this._stationInfo.numberOfConnectors as number; } else { @@ -293,7 +315,7 @@ export default class ChargingStation { return !Utils.isUndefined(this._stationInfo.voltageOut) ? Utils.convertToInt(this._stationInfo.voltageOut) : defaultVoltageOut; } - _getTransactionidTag(transactionId: number): string { + _getTransactionIdTag(transactionId: number): string { for (const connector in this._connectors) { if (this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) { return this.getConnector(Utils.convertToInt(connector)).idTag; @@ -306,20 +328,24 @@ export default class ChargingStation { } _getSupervisionURL(): string { - const supervisionUrls = Utils.cloneObject(this._stationInfo.supervisionURL ? this._stationInfo.supervisionURL : Configuration.getSupervisionURLs()) as string|string[]; + const supervisionUrls = Utils.cloneObject(this._stationInfo.supervisionURL ? this._stationInfo.supervisionURL : Configuration.getSupervisionURLs()) as string | string[]; let indexUrl = 0; if (!Utils.isEmptyArray(supervisionUrls)) { - if (Configuration.getDistributeStationToTenantEqually()) { + if (Configuration.getDistributeStationsToTenantsEqually()) { indexUrl = this._index % supervisionUrls.length; } else { // Get a random url indexUrl = Math.floor(Math.random() * supervisionUrls.length); } - return supervisionUrls[indexUrl] ; + return supervisionUrls[indexUrl]; } return supervisionUrls as string; } + _getReconnectExponentialDelay(): boolean { + return !Utils.isUndefined(this._stationInfo.reconnectExponentialDelay) ? this._stationInfo.reconnectExponentialDelay : false; + } + _getAuthorizeRemoteTxRequests(): boolean { const authorizeRemoteTxRequests = this._getConfigurationKey('AuthorizeRemoteTxRequests'); return authorizeRemoteTxRequests ? Utils.convertToBoolean(authorizeRemoteTxRequests.value) : false; @@ -330,21 +356,25 @@ export default class ChargingStation { return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false; } - _startMessageSequence(): void { + async _startMessageSequence(): Promise { + // Start WebSocket ping + this._startWebSocketPing(); // Start heartbeat this._startHeartbeat(); // Initialize connectors status for (const connector in this._connectors) { - if (!this.getConnector(Utils.convertToInt(connector)).transactionStarted) { - if (!this.getConnector(Utils.convertToInt(connector)).status && this.getConnector(Utils.convertToInt(connector)).bootStatus) { - this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).bootStatus); - } else if (!this._hasStopped && this.getConnector(Utils.convertToInt(connector)).status) { - this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).status); - } else { - this.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.AVAILABLE); - } + if (!this._hasStopped && !this.getConnector(Utils.convertToInt(connector)).status && this.getConnector(Utils.convertToInt(connector)).bootStatus) { + // Send status in template at startup + await this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).bootStatus); + } else if (this._hasStopped && this.getConnector(Utils.convertToInt(connector)).bootStatus) { + // Send status in template after reset + await this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).bootStatus); + } else if (!this._hasStopped && this.getConnector(Utils.convertToInt(connector)).status) { + // Send previous status at template reload + await this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).status); } else { - this.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.CHARGING); + // Send default status + await this.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.AVAILABLE); } } // Start the ATG @@ -362,6 +392,8 @@ export default class ChargingStation { } async _stopMessageSequence(reason: StopTransactionReason = StopTransactionReason.NONE): Promise { + // Stop WebSocket ping + this._stopWebSocketPing(); // Stop heartbeat this._stopHeartbeat(); // Stop the ATG @@ -378,14 +410,46 @@ export default class ChargingStation { } } + _startWebSocketPing(): void { + const webSocketPingInterval: number = this._getConfigurationKey('WebSocketPingInterval') ? Utils.convertToInt(this._getConfigurationKey('WebSocketPingInterval').value) : 0; + if (webSocketPingInterval > 0 && !this._webSocketPingSetInterval) { + this._webSocketPingSetInterval = setInterval(() => { + if (this._wsConnection?.readyState === WebSocket.OPEN) { + this._wsConnection.ping((): void => { }); + } + }, webSocketPingInterval * 1000); + logger.info(this._logPrefix() + ' WebSocket ping started every ' + Utils.secondsToHHMMSS(webSocketPingInterval)); + } else if (this._webSocketPingSetInterval) { + logger.info(this._logPrefix() + ' WebSocket ping every ' + Utils.secondsToHHMMSS(webSocketPingInterval) + ' already started'); + } else { + logger.error(`${this._logPrefix()} WebSocket ping interval set to ${webSocketPingInterval ? Utils.secondsToHHMMSS(webSocketPingInterval) : webSocketPingInterval}, not starting the WebSocket ping`); + } + } + + _stopWebSocketPing(): void { + if (this._webSocketPingSetInterval) { + clearInterval(this._webSocketPingSetInterval); + this._webSocketPingSetInterval = null; + } + } + + _restartWebSocketPing(): void { + // Stop WebSocket ping + this._stopWebSocketPing(); + // Start WebSocket ping + this._startWebSocketPing(); + } + _startHeartbeat(): void { if (this._heartbeatInterval && this._heartbeatInterval > 0 && !this._heartbeatSetInterval) { - this._heartbeatSetInterval = setInterval(() => { - this.sendHeartbeat(); + this._heartbeatSetInterval = setInterval(async () => { + await this.sendHeartbeat(); }, this._heartbeatInterval); logger.info(this._logPrefix() + ' Heartbeat started every ' + Utils.milliSecondsToHHMMSS(this._heartbeatInterval)); + } else if (this._heartbeatSetInterval) { + logger.info(this._logPrefix() + ' Heartbeat every ' + Utils.milliSecondsToHHMMSS(this._heartbeatInterval) + ' already started'); } else { - logger.error(`${this._logPrefix()} Heartbeat interval set to ${Utils.milliSecondsToHHMMSS(this._heartbeatInterval)}, not starting the heartbeat`); + logger.error(`${this._logPrefix()} Heartbeat interval set to ${this._heartbeatInterval ? Utils.milliSecondsToHHMMSS(this._heartbeatInterval) : this._heartbeatInterval}, not starting the heartbeat`); } } @@ -396,6 +460,13 @@ export default class ChargingStation { } } + _restartHeartbeat(): void { + // Stop heartbeat + this._stopHeartbeat(); + // Start heartbeat + this._startHeartbeat(); + } + _startAuthorizationFileMonitoring(): void { // eslint-disable-next-line @typescript-eslint/no-unused-vars fs.watchFile(this._getAuthorizationFile(), (current, previous) => { @@ -404,7 +475,7 @@ export default class ChargingStation { // Initialize _authorizedTags this._authorizedTags = this._loadAndGetAuthorizedTags(); } catch (error) { - logger.error(this._logPrefix() + ' Authorization file monitoring error: ' + error); + logger.error(this._logPrefix() + ' Authorization file monitoring error: %j', error); } }); } @@ -420,8 +491,9 @@ export default class ChargingStation { this._automaticTransactionGeneration) { this._automaticTransactionGeneration.stop().catch(() => { }); } + // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed } catch (error) { - logger.error(this._logPrefix() + ' Charging station template file monitoring error: ' + error); + logger.error(this._logPrefix() + ' Charging station template file monitoring error: %j', error); } }); } @@ -451,12 +523,19 @@ export default class ChargingStation { } } - start(): void { - if (!this._wsConnectionUrl) { - this._wsConnectionUrl = this._supervisionUrl + '/' + this._stationInfo.name; + _openWSConnection(options?: WebSocket.ClientOptions): void { + if (Utils.isUndefined(options)) { + options = {} as WebSocket.ClientOptions; + } + if (Utils.isUndefined(options.handshakeTimeout)) { + options.handshakeTimeout = this._connectionTimeout; } - this._wsConnection = new WebSocket(this._wsConnectionUrl, 'ocpp' + Constants.OCPP_VERSION_16); + this._wsConnection = new WebSocket(this._wsConnectionUrl, 'ocpp' + Constants.OCPP_VERSION_16, options); logger.info(this._logPrefix() + ' Will communicate through URL ' + this._supervisionUrl); + } + + start(): void { + this._openWSConnection(); // Monitor authorization file this._startAuthorizationFileMonitoring(); // Monitor station template file @@ -471,56 +550,58 @@ export default class ChargingStation { this._wsConnection.on('open', this.onOpen.bind(this)); // Handle Socket ping this._wsConnection.on('ping', this.onPing.bind(this)); + // Handle Socket pong + this._wsConnection.on('pong', this.onPong.bind(this)); } async stop(reason: StopTransactionReason = StopTransactionReason.NONE): Promise { - // Stop + // Stop message sequence await this._stopMessageSequence(reason); // eslint-disable-next-line guard-for-in for (const connector in this._connectors) { await this.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.UNAVAILABLE); } - if (this._wsConnection && this._wsConnection.readyState === WebSocket.OPEN) { + if (this._wsConnection?.readyState === WebSocket.OPEN) { this._wsConnection.close(); } this._hasStopped = true; } - _reconnect(error): void { - logger.error(this._logPrefix() + ' Socket: abnormally closed', error); + async _reconnect(error): Promise { + logger.error(this._logPrefix() + ' Socket: abnormally closed: %j', error); + // Stop heartbeat + this._stopHeartbeat(); // Stop the ATG if needed if (this._stationInfo.AutomaticTransactionGenerator.enable && this._stationInfo.AutomaticTransactionGenerator.stopOnConnectionFailure && this._automaticTransactionGeneration && !this._automaticTransactionGeneration.timeToStop) { - this._automaticTransactionGeneration.stop().catch(() => {}); + this._automaticTransactionGeneration.stop().catch(() => { }); } - // Stop heartbeat - this._stopHeartbeat(); - if (this._autoReconnectTimeout !== 0 && - (this._autoReconnectRetryCount < this._autoReconnectMaxRetries || this._autoReconnectMaxRetries === -1)) { - logger.error(`${this._logPrefix()} Socket: connection retry with timeout ${this._autoReconnectTimeout}ms`); + if (this._autoReconnectRetryCount < this._autoReconnectMaxRetries || this._autoReconnectMaxRetries === -1) { this._autoReconnectRetryCount++; - setTimeout(() => { - logger.error(this._logPrefix() + ' Socket: reconnecting try #' + this._autoReconnectRetryCount.toString()); - this.start(); - }, this._autoReconnectTimeout); - } else if (this._autoReconnectTimeout !== 0 || this._autoReconnectMaxRetries !== -1) { - logger.error(`${this._logPrefix()} Socket: max retries reached (${this._autoReconnectRetryCount}) or retry disabled (${this._autoReconnectTimeout})`); + const reconnectDelay = (this._getReconnectExponentialDelay() ? Utils.exponentialDelay(this._autoReconnectRetryCount) : this._connectionTimeout); + logger.error(`${this._logPrefix()} Socket: connection retry in ${Utils.roundTo(reconnectDelay, 2)}ms, timeout ${reconnectDelay - 100}ms`); + await Utils.sleep(reconnectDelay); + logger.error(this._logPrefix() + ' Socket: reconnecting try #' + this._autoReconnectRetryCount.toString()); + this._openWSConnection({ handshakeTimeout: reconnectDelay - 100 }); + } else if (this._autoReconnectMaxRetries !== -1) { + logger.error(`${this._logPrefix()} Socket: max retries reached (${this._autoReconnectRetryCount}) or retry disabled (${this._autoReconnectMaxRetries})`); } } - onOpen(): void { + async onOpen(): Promise { logger.info(`${this._logPrefix()} Is connected to server through ${this._wsConnectionUrl}`); - if (!this._hasSocketRestarted) { + if (!this._hasSocketRestarted || this._hasStopped) { // Send BootNotification - this.sendBootNotification(); + await this.sendBootNotification(); } + await this._startMessageSequence(); if (this._hasSocketRestarted) { - this._startMessageSequence(); if (!Utils.isEmptyArray(this._messageQueue)) { - this._messageQueue.forEach((message) => { - if (this._wsConnection && this._wsConnection.readyState === WebSocket.OPEN) { + this._messageQueue.forEach((message, index) => { + if (this._wsConnection?.readyState === WebSocket.OPEN) { + this._messageQueue.splice(index, 1); this._wsConnection.send(message); } }); @@ -530,28 +611,28 @@ export default class ChargingStation { this._hasSocketRestarted = false; } - onError(error): void { - switch (error) { + async onError(errorEvent): Promise { + switch (errorEvent.code) { case 'ECONNREFUSED': this._hasSocketRestarted = true; - this._reconnect(error); + await this._reconnect(errorEvent); break; default: - logger.error(this._logPrefix() + ' Socket error: ' + error); + logger.error(this._logPrefix() + ' Socket error: %j', errorEvent); break; } } - onClose(error): void { - switch (error) { + async onClose(closeEvent): Promise { + switch (closeEvent) { case 1000: // Normal close case 1005: - logger.info(this._logPrefix() + ' Socket normally closed ' + error); + logger.info(this._logPrefix() + ' Socket normally closed: %j', closeEvent); this._autoReconnectRetryCount = 0; break; default: // Abnormal close this._hasSocketRestarted = true; - this._reconnect(error); + await this._reconnect(closeEvent); break; } } @@ -560,11 +641,15 @@ export default class ChargingStation { logger.debug(this._logPrefix() + ' Has received a WS ping (rfc6455) from the server'); } - async onMessage(message): Promise { + onPong(): void { + logger.debug(this._logPrefix() + ' Has received a WS pong (rfc6455) from the server'); + } + + async onMessage(messageEvent: MessageEvent): Promise { let [messageType, messageId, commandName, commandPayload, errorDetails] = [0, '', Constants.ENTITY_CHARGING_STATION, '', '']; try { // Parse the message - [messageType, messageId, commandName, commandPayload, errorDetails] = JSON.parse(message); + [messageType, messageId, commandName, commandPayload, errorDetails] = JSON.parse(messageEvent.toString()); // Check the Type of message switch (messageType) { @@ -618,9 +703,9 @@ export default class ChargingStation { } } catch (error) { // Log - logger.error('%s Incoming message %j processing error %s on request content type %s', this._logPrefix(), message, error, this._requests[messageId]); + logger.error('%s Incoming message %j processing error %s on request content type %s', this._logPrefix(), messageEvent, error, this._requests[messageId]); // Send error - await this.sendError(messageId, error, commandName); + messageType !== Constants.OCPP_JSON_CALL_ERROR_MESSAGE && await this.sendError(messageId, error, commandName); } } @@ -631,7 +716,7 @@ export default class ChargingStation { }; await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'Heartbeat'); } catch (error) { - logger.error(this._logPrefix() + ' Send Heartbeat error: ' + error); + logger.error(this._logPrefix() + ' Send Heartbeat error: %j', error); throw error; } } @@ -640,7 +725,7 @@ export default class ChargingStation { try { await this.sendMessage(Utils.generateUUID(), this._bootNotificationMessage, Constants.OCPP_JSON_CALL_MESSAGE, 'BootNotification'); } catch (error) { - logger.error(this._logPrefix() + ' Send BootNotification error: ' + error); + logger.error(this._logPrefix() + ' Send BootNotification error: %j', error); throw error; } } @@ -655,7 +740,7 @@ export default class ChargingStation { }; await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StatusNotification'); } catch (error) { - logger.error(this._logPrefix() + ' Send StatusNotification error: ' + error); + logger.error(this._logPrefix() + ' Send StatusNotification error: %j', error); throw error; } } @@ -670,13 +755,13 @@ export default class ChargingStation { }; return await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StartTransaction') as StartTransactionResponse; } catch (error) { - logger.error(this._logPrefix() + ' Send StartTransaction error: ' + error); + logger.error(this._logPrefix() + ' Send StartTransaction error: %j', error); throw error; } } async sendStopTransaction(transactionId: number, reason: StopTransactionReason = StopTransactionReason.NONE): Promise { - const idTag = this._getTransactionidTag(transactionId); + const idTag = this._getTransactionIdTag(transactionId); try { const payload = { transactionId, @@ -687,7 +772,7 @@ export default class ChargingStation { }; return await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StopTransaction') as StartTransactionResponse; } catch (error) { - logger.error(this._logPrefix() + ' Send StopTransaction error: ' + error); + logger.error(this._logPrefix() + ' Send StopTransaction error: %j', error); throw error; } } @@ -801,7 +886,7 @@ export default class ChargingStation { ...!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}`] }, + ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: powerMeasurandValues[`L${phase}`] as string }, phase: phaseValue as MeterValuePhase, }); } @@ -862,7 +947,7 @@ export default class ChargingStation { ...!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] }, + ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: currentMeasurandValues[phaseValue] as string }, phase: phaseValue as MeterValuePhase, }); } @@ -892,7 +977,8 @@ export default class ChargingStation { ...!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.toString() }, + ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : + { value: connector.lastEnergyActiveImportRegisterValue.toString() }, }); const sampledValuesIndex = sampledValues.sampledValue.length - 1; const maxConsumption = Math.round(self._stationInfo.maxPower * 3600 / (self._stationInfo.powerDivider * interval)); @@ -912,7 +998,7 @@ export default class ChargingStation { }; await self.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'MeterValues'); } catch (error) { - logger.error(self._logPrefix() + ' Send MeterValues error: ' + error); + logger.error(self._logPrefix() + ' Send MeterValues error: %j', error); throw error; } } @@ -950,7 +1036,7 @@ export default class ChargingStation { break; } // Check if wsConnection is ready - if (this._wsConnection && this._wsConnection.readyState === WebSocket.OPEN) { + if (this._wsConnection?.readyState === WebSocket.OPEN) { if (this.getEnableStatistics()) { this._statistics.addMessage(commandName, messageType); } @@ -971,7 +1057,7 @@ export default class ChargingStation { this._messageQueue.push(messageToSend); } // Reject it - return rejectCallback(new OCPPError(commandParams.code ? commandParams.code : Constants.OCPP_ERROR_GENERIC_ERROR, commandParams.message ? commandParams.message : `Web socket closed for message id '${messageId}' with content '${messageToSend}', message buffered`, commandParams.details ? commandParams.details : {})); + return rejectCallback(new OCPPError(commandParams.code ? commandParams.code : Constants.OCPP_ERROR_GENERIC_ERROR, commandParams.message ? commandParams.message : `WebSocket closed for message id '${messageId}' with content '${messageToSend}', message buffered`, commandParams.details ? commandParams.details : {})); } // Response? if (messageType === Constants.OCPP_JSON_CALL_RESULT_MESSAGE) { @@ -997,7 +1083,7 @@ export default class ChargingStation { if (self.getEnableStatistics()) { self._statistics.addMessage(commandName, messageType); } - logger.debug(`${self._logPrefix()} Error %j occurred when calling command %s with parameters %j`, error, commandName, commandParams); + logger.debug(`${self._logPrefix()} Error: %j occurred when calling command %s with parameters: %j`, error, commandName, commandParams); // Build Exception // eslint-disable-next-line no-empty-function self._requests[messageId] = [() => { }, () => { }, {}]; // Properly format the request @@ -1018,10 +1104,10 @@ export default class ChargingStation { handleResponseBootNotification(payload, requestPayload): void { if (payload.status === 'Accepted') { - this._heartbeatInterval = payload.interval * 1000; + this._heartbeatInterval = Utils.convertToInt(payload.interval) * 1000; + this._heartbeatSetInterval ? this._restartHeartbeat() : this._startHeartbeat(); this._addConfigurationKey('HeartBeatInterval', payload.interval); this._addConfigurationKey('HeartbeatInterval', payload.interval, false, false); - this._startMessageSequence(); this._hasStopped && (this._hasStopped = false); } else if (payload.status === 'Pending') { logger.info(this._logPrefix() + ' Charging station in pending state on the central server'); @@ -1045,56 +1131,57 @@ export default class ChargingStation { } handleResponseStartTransaction(payload: StartTransactionResponse, requestPayload): void { - 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)); + const connectorId = Utils.convertToInt(requestPayload.connectorId); + if (this.getConnector(connectorId).transactionStarted) { + logger.debug(this._logPrefix() + ' Trying to start a transaction on an already used connector ' + connectorId.toString() + ': %j', this.getConnector(connectorId)); return; } let transactionConnectorId: number; for (const connector in this._connectors) { - if (Utils.convertToInt(connector) === Utils.convertToInt(requestPayload.connectorId)) { + if (Utils.convertToInt(connector) === connectorId) { transactionConnectorId = Utils.convertToInt(connector); break; } } if (!transactionConnectorId) { - logger.error(this._logPrefix() + ' Try to start a transaction on a non existing connector Id ' + requestPayload.connectorId); + logger.error(this._logPrefix() + ' Trying to start a transaction on a non existing connector Id ' + connectorId.toString()); return; } if (payload.idTagInfo?.status === AuthorizationStatus.ACCEPTED) { - 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, ChargePointStatus.CHARGING); - logger.info(this._logPrefix() + ' Transaction ' + payload.transactionId + ' STARTED on ' + this._stationInfo.name + '#' + requestPayload.connectorId + ' for idTag ' + requestPayload.idTag); + this.getConnector(connectorId).transactionStarted = true; + this.getConnector(connectorId).transactionId = payload.transactionId; + this.getConnector(connectorId).idTag = requestPayload.idTag; + this.getConnector(connectorId).lastEnergyActiveImportRegisterValue = 0; + this.sendStatusNotification(connectorId, ChargePointStatus.CHARGING).catch(() => { }); + logger.info(this._logPrefix() + ' Transaction ' + payload.transactionId.toString() + ' STARTED on ' + this._stationInfo.name + '#' + connectorId.toString() + ' for idTag ' + requestPayload.idTag); if (this._stationInfo.powerSharedByConnectors) { this._stationInfo.powerDivider++; } const configuredMeterValueSampleInterval = this._getConfigurationKey('MeterValueSampleInterval'); - this._startMeterValues(requestPayload.connectorId, + this._startMeterValues(connectorId, configuredMeterValueSampleInterval ? Utils.convertToInt(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(requestPayload.connectorId); - this.sendStatusNotification(requestPayload.connectorId, ChargePointStatus.AVAILABLE); + logger.error(this._logPrefix() + ' Starting transaction id ' + payload.transactionId.toString() + ' REJECTED with status ' + payload.idTagInfo?.status + ', idTag ' + requestPayload.idTag); + this._resetTransactionOnConnector(connectorId); + this.sendStatusNotification(connectorId, ChargePointStatus.AVAILABLE).catch(() => { }); } } handleResponseStopTransaction(payload: StopTransactionResponse, requestPayload): void { let transactionConnectorId: number; for (const connector in this._connectors) { - if (this.getConnector(Utils.convertToInt(connector)).transactionId === requestPayload.transactionId) { + if (this.getConnector(Utils.convertToInt(connector)).transactionId === Utils.convertToInt(requestPayload.transactionId)) { transactionConnectorId = Utils.convertToInt(connector); break; } } if (!transactionConnectorId) { - logger.error(this._logPrefix() + ' Try to stop a non existing transaction ' + requestPayload.transactionId); + logger.error(this._logPrefix() + ' Trying to stop a non existing transaction ' + requestPayload.transactionId); return; } if (payload.idTagInfo?.status === AuthorizationStatus.ACCEPTED) { - this.sendStatusNotification(transactionConnectorId, ChargePointStatus.AVAILABLE); + this.sendStatusNotification(transactionConnectorId, ChargePointStatus.AVAILABLE).catch(() => { }); if (this._stationInfo.powerSharedByConnectors) { this._stationInfo.powerDivider--; } @@ -1126,7 +1213,7 @@ export default class ChargingStation { response = await this['handleRequest' + commandName](commandPayload); } catch (error) { // Log - logger.error(this._logPrefix() + ' Handle request error: ' + error); + logger.error(this._logPrefix() + ' Handle request error: %j', error); // Send back response to inform backend await this.sendError(messageId, error, commandName); throw error; @@ -1154,7 +1241,7 @@ export default class ChargingStation { async handleRequestUnlockConnector(commandPayload): Promise { const connectorId = Utils.convertToInt(commandPayload.connectorId); if (connectorId === 0) { - logger.error(this._logPrefix() + ' Try to unlock connector ' + connectorId.toString()); + logger.error(this._logPrefix() + ' Trying to unlock connector ' + connectorId.toString()); return Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED; } if (this.getConnector(connectorId).transactionStarted) { @@ -1244,22 +1331,26 @@ export default class ChargingStation { return Constants.OCPP_CONFIGURATION_RESPONSE_REJECTED; } else if (keyToChange && !keyToChange.readonly) { const keyIndex = this._configuration.configurationKey.indexOf(keyToChange); - this._configuration.configurationKey[keyIndex].value = commandPayload.value; + let valueChanged = false; + if (this._configuration.configurationKey[keyIndex].value !== commandPayload.value) { + this._configuration.configurationKey[keyIndex].value = commandPayload.value as string; + valueChanged = true; + } let triggerHeartbeatRestart = false; - if (keyToChange.key === 'HeartBeatInterval') { + if (keyToChange.key === 'HeartBeatInterval' && valueChanged) { this._setConfigurationKeyValue('HeartbeatInterval', commandPayload.value); triggerHeartbeatRestart = true; } - if (keyToChange.key === 'HeartbeatInterval') { + if (keyToChange.key === 'HeartbeatInterval' && valueChanged) { this._setConfigurationKeyValue('HeartBeatInterval', commandPayload.value); triggerHeartbeatRestart = true; } if (triggerHeartbeatRestart) { this._heartbeatInterval = Utils.convertToInt(commandPayload.value) * 1000; - // Stop heartbeat - this._stopHeartbeat(); - // Start heartbeat - this._startHeartbeat(); + this._restartHeartbeat(); + } + if (keyToChange.key === 'WebSocketPingInterval' && valueChanged) { + this._restartWebSocketPing(); } if (keyToChange.reboot) { return Constants.OCPP_CONFIGURATION_RESPONSE_REBOOT_REQUIRED; @@ -1295,7 +1386,7 @@ export default class ChargingStation { return Constants.OCPP_RESPONSE_ACCEPTED; } } - logger.info(this._logPrefix() + ' Try to stop remotely a non existing transaction ' + commandPayload.transactionId); + logger.info(this._logPrefix() + ' Trying to remote stop a non existing transaction ' + transactionId.toString()); return Constants.OCPP_RESPONSE_REJECTED; } }