X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2FChargingStation.ts;h=1494cc56365d983ec779c50dbd858af18cad739f;hb=b2b606263e2676354259164d532ff9aa91ccdf87;hp=9cd20129714e214efb905ff8c68a6ff012389ba8;hpb=4334db724447c1a0f989cd467dc44e136a7478eb;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 9cd20129..1494cc56 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -1,39 +1,61 @@ // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved. -import crypto from 'node:crypto'; -import fs from 'node:fs'; -import path from 'node:path'; +import { createHash } from 'node:crypto'; +import { + type FSWatcher, + closeSync, + existsSync, + mkdirSync, + openSync, + readFileSync, + writeFileSync, +} from 'node:fs'; +import { dirname, join } from 'node:path'; import { URL } from 'node:url'; import { parentPort } from 'node:worker_threads'; import merge from 'just-merge'; -import WebSocket, { type RawData } from 'ws'; +import { type RawData, WebSocket } from 'ws'; +import { AutomaticTransactionGenerator } from './AutomaticTransactionGenerator'; +import { ChargingStationWorkerBroadcastChannel } from './broadcast-channel/ChargingStationWorkerBroadcastChannel'; +import { ChargingStationConfigurationUtils } from './ChargingStationConfigurationUtils'; import { - AutomaticTransactionGenerator, - ChargingStationConfigurationUtils, - ChargingStationUtils, - ChargingStationWorkerBroadcastChannel, - IdTagsCache, - MessageChannelUtils, - SharedLRUCache, -} from './internal'; + buildConnectorsMap, + checkConnectorsConfiguration, + checkStationInfoConnectorStatus, + checkTemplate, + countReservableConnectors, + createBootNotificationRequest, + createSerialNumber, + getAmperageLimitationUnitDivider, + getBootConnectorStatus, + getChargingStationConnectorChargingProfilesPowerLimit, + getChargingStationId, + getDefaultVoltageOut, + getHashId, + getIdTagsFile, + getMaxNumberOfEvses, + getPhaseRotationValue, + initializeConnectorsMapStatus, + propagateSerialNumber, + stationTemplateToStationInfo, + warnTemplateKeysDeprecation, +} from './ChargingStationUtils'; +import { IdTagsCache } from './IdTagsCache'; import { - // OCPP16IncomingRequestService, + OCPP16IncomingRequestService, OCPP16RequestService, - // OCPP16ResponseService, + OCPP16ResponseService, OCPP16ServiceUtils, OCPP20IncomingRequestService, OCPP20RequestService, - // OCPP20ResponseService, + OCPP20ResponseService, type OCPPIncomingRequestService, type OCPPRequestService, - // OCPPServiceUtils, + OCPPServiceUtils, } from './ocpp'; -import { OCPP16IncomingRequestService } from './ocpp/1.6/OCPP16IncomingRequestService'; -import { OCPP16ResponseService } from './ocpp/1.6/OCPP16ResponseService'; -import { OCPP20ResponseService } from './ocpp/2.0/OCPP20ResponseService'; -import { OCPPServiceUtils } from './ocpp/OCPPServiceUtils'; +import { SharedLRUCache } from './SharedLRUCache'; import { BaseError, OCPPError } from '../exception'; import { PerformanceStatistics } from '../performance'; import { @@ -74,8 +96,12 @@ import { PowerUnits, RegistrationStatusEnumType, RequestCommand, + type Reservation, + ReservationFilterKey, + ReservationTerminationReason, type Response, StandardParametersKey, + type Status, type StatusNotificationRequest, type StatusNotificationResponse, StopTransactionReason, @@ -90,12 +116,36 @@ import { } from '../types'; import { ACElectricUtils, + AsyncLock, + AsyncLockType, Configuration, Constants, DCElectricUtils, - FileUtils, - Utils, + buildChargingStationAutomaticTransactionGeneratorConfiguration, + buildConnectorsStatus, + buildEvsesStatus, + buildStartedMessage, + buildStoppedMessage, + buildUpdatedMessage, + cloneObject, + convertToBoolean, + convertToInt, + exponentialDelay, + formatDurationMilliSeconds, + formatDurationSeconds, + getRandomInteger, + getWebSocketCloseEventStatusString, + handleFileException, + isNotEmptyArray, + isNotEmptyString, + isNullOrUndefined, + isUndefined, + logPrefix, logger, + roundTo, + secureRandom, + sleep, + watchJsonFile, } from '../utils'; export class ChargingStation { @@ -127,10 +177,12 @@ export class ChargingStation { private configuredSupervisionUrl!: URL; private wsConnectionRestarted: boolean; private autoReconnectRetryCount: number; - private templateFileWatcher!: fs.FSWatcher | undefined; + private templateFileWatcher!: FSWatcher | undefined; + private templateFileHash!: string; private readonly sharedLRUCache: SharedLRUCache; private webSocketPingSetInterval!: NodeJS.Timeout; private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel; + private reservationExpirationSetInterval?: NodeJS.Timeout; constructor(index: number, templateFile: string) { this.started = false; @@ -159,30 +211,35 @@ export class ChargingStation { return new URL( `${ this.getSupervisionUrlOcppConfiguration() && - Utils.isNotEmptyString(this.getSupervisionUrlOcppKey()) + isNotEmptyString(this.getSupervisionUrlOcppKey()) && + isNotEmptyString( + ChargingStationConfigurationUtils.getConfigurationKey( + this, + this.getSupervisionUrlOcppKey() + )?.value + ) ? ChargingStationConfigurationUtils.getConfigurationKey( this, this.getSupervisionUrlOcppKey() - )?.value + ).value : this.configuredSupervisionUrl.href }/${this.stationInfo.chargingStationId}` ); } public logPrefix = (): string => { - return Utils.logPrefix( + return logPrefix( ` ${ - (Utils.isNotEmptyString(this?.stationInfo?.chargingStationId) + (isNotEmptyString(this?.stationInfo?.chargingStationId) ? this?.stationInfo?.chargingStationId - : ChargingStationUtils.getChargingStationId(this.index, this.getTemplateFromFile())) ?? + : getChargingStationId(this.index, this.getTemplateFromFile())) ?? 'Error at building log prefix' } |` ); }; public hasIdTags(): boolean { - const idTagsFile = ChargingStationUtils.getIdTagsFile(this.stationInfo); - return Utils.isNotEmptyArray(this.idTagsCache.getIdTags(idTagsFile)); + return isNotEmptyArray(this.idTagsCache.getIdTags(getIdTagsFile(this.stationInfo))); } public getEnableStatistics(): boolean { @@ -193,17 +250,11 @@ export class ChargingStation { return this.stationInfo.mustAuthorizeAtRemoteStart ?? true; } - public getPayloadSchemaValidation(): boolean { - return this.stationInfo.payloadSchemaValidation ?? true; - } - public getNumberOfPhases(stationInfo?: ChargingStationInfo): number | undefined { const localStationInfo: ChargingStationInfo = stationInfo ?? this.stationInfo; switch (this.getCurrentOutType(stationInfo)) { case CurrentType.AC: - return !Utils.isUndefined(localStationInfo.numberOfPhases) - ? localStationInfo.numberOfPhases - : 3; + return !isUndefined(localStationInfo.numberOfPhases) ? localStationInfo.numberOfPhases : 3; case CurrentType.DC: return 0; } @@ -217,26 +268,26 @@ export class ChargingStation { return this?.bootNotificationResponse?.status; } - public isInUnknownState(): boolean { - return Utils.isNullOrUndefined(this?.bootNotificationResponse?.status); + public inUnknownState(): boolean { + return isNullOrUndefined(this?.bootNotificationResponse?.status); } - public isInPendingState(): boolean { + public inPendingState(): boolean { return this?.bootNotificationResponse?.status === RegistrationStatusEnumType.PENDING; } - public isInAcceptedState(): boolean { + public inAcceptedState(): boolean { return this?.bootNotificationResponse?.status === RegistrationStatusEnumType.ACCEPTED; } - public isInRejectedState(): boolean { + public inRejectedState(): boolean { return this?.bootNotificationResponse?.status === RegistrationStatusEnumType.REJECTED; } public isRegistered(): boolean { return ( - this.isInUnknownState() === false && - (this.isInAcceptedState() === true || this.isInPendingState() === true) + this.inUnknownState() === false && + (this.inAcceptedState() === true || this.inPendingState() === true) ); } @@ -287,6 +338,7 @@ export class ChargingStation { return evseStatus.connectors.get(connectorId); } } + return undefined; } return this.connectors.get(connectorId); } @@ -296,19 +348,16 @@ export class ChargingStation { } public getOcppStrictCompliance(): boolean { - return this.stationInfo?.ocppStrictCompliance ?? false; + return this.stationInfo?.ocppStrictCompliance ?? true; } public getVoltageOut(stationInfo?: ChargingStationInfo): number | undefined { - const defaultVoltageOut = ChargingStationUtils.getDefaultVoltageOut( + const defaultVoltageOut = getDefaultVoltageOut( this.getCurrentOutType(stationInfo), - this.templateFile, - this.logPrefix() + this.logPrefix(), + this.templateFile ); - const localStationInfo: ChargingStationInfo = stationInfo ?? this.stationInfo; - return !Utils.isUndefined(localStationInfo.voltageOut) - ? localStationInfo.voltageOut - : defaultVoltageOut; + return (stationInfo ?? this.stationInfo).voltageOut ?? defaultVoltageOut; } public getMaximumPower(stationInfo?: ChargingStationInfo): number { @@ -319,7 +368,7 @@ export class ChargingStation { public getConnectorMaximumAvailablePower(connectorId: number): number { let connectorAmperageLimitationPowerLimit: number; if ( - !Utils.isNullOrUndefined(this.getAmperageLimitation()) && + !isNullOrUndefined(this.getAmperageLimitation()) && this.getAmperageLimitation() < this.stationInfo?.maximumAmperage ) { connectorAmperageLimitationPowerLimit = @@ -327,14 +376,15 @@ export class ChargingStation { ? ACElectricUtils.powerTotal( this.getNumberOfPhases(), this.getVoltageOut(), - this.getAmperageLimitation() * this.getNumberOfConnectors() + this.getAmperageLimitation() * + (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()) ) : DCElectricUtils.power(this.getVoltageOut(), this.getAmperageLimitation())) / this.powerDivider; } const connectorMaximumPower = this.getMaximumPower() / this.powerDivider; const connectorChargingProfilesPowerLimit = - ChargingStationUtils.getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId); + getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId); return Math.min( isNaN(connectorMaximumPower) ? Infinity : connectorMaximumPower, isNaN(connectorAmperageLimitationPowerLimit) @@ -355,16 +405,36 @@ export class ChargingStation { } } else { for (const connectorId of this.connectors.keys()) { - if ( - connectorId > 0 && - this.getConnectorStatus(connectorId)?.transactionId === transactionId - ) { + if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) { return this.getConnectorStatus(connectorId)?.transactionIdTag; } } } } + public getNumberOfRunningTransactions(): number { + let trxCount = 0; + if (this.hasEvses) { + for (const [evseId, evseStatus] of this.evses) { + if (evseId === 0) { + continue; + } + for (const connectorStatus of evseStatus.connectors.values()) { + if (connectorStatus.transactionStarted === true) { + ++trxCount; + } + } + } + } else { + for (const connectorId of this.connectors.keys()) { + if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) { + ++trxCount; + } + } + } + return trxCount; + } + public getOutOfOrderEndMeterValues(): boolean { return this.stationInfo?.outOfOrderEndMeterValues ?? false; } @@ -404,10 +474,7 @@ export class ChargingStation { } } else { for (const connectorId of this.connectors.keys()) { - if ( - connectorId > 0 && - this.getConnectorStatus(connectorId)?.transactionId === transactionId - ) { + if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) { return connectorId; } } @@ -433,9 +500,7 @@ export class ChargingStation { this, StandardParametersKey.AuthorizeRemoteTxRequests ); - return authorizeRemoteTxRequests - ? Utils.convertToBoolean(authorizeRemoteTxRequests.value) - : false; + return authorizeRemoteTxRequests ? convertToBoolean(authorizeRemoteTxRequests.value) : false; } public getLocalAuthListEnabled(): boolean { @@ -443,7 +508,7 @@ export class ChargingStation { this, StandardParametersKey.LocalAuthListEnabled ); - return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false; + return localAuthListEnabled ? convertToBoolean(localAuthListEnabled.value) : false; } public getHeartbeatInterval(): number { @@ -452,14 +517,14 @@ export class ChargingStation { StandardParametersKey.HeartbeatInterval ); if (HeartbeatInterval) { - return Utils.convertToInt(HeartbeatInterval.value) * 1000; + return convertToInt(HeartbeatInterval.value) * 1000; } const HeartBeatInterval = ChargingStationConfigurationUtils.getConfigurationKey( this, StandardParametersKey.HeartBeatInterval ); if (HeartBeatInterval) { - return Utils.convertToInt(HeartBeatInterval.value) * 1000; + return convertToInt(HeartBeatInterval.value) * 1000; } this.stationInfo?.autoRegister === false && logger.warn( @@ -473,7 +538,7 @@ export class ChargingStation { public setSupervisionUrl(url: string): void { if ( this.getSupervisionUrlOcppConfiguration() && - Utils.isNotEmptyString(this.getSupervisionUrlOcppKey()) + isNotEmptyString(this.getSupervisionUrlOcppKey()) ) { ChargingStationConfigurationUtils.setConfigurationKeyValue( this, @@ -500,19 +565,20 @@ export class ChargingStation { }); }, this.getHeartbeatInterval()); logger.info( - `${this.logPrefix()} Heartbeat started every ${Utils.formatDurationMilliSeconds( + `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds( this.getHeartbeatInterval() )}` ); } else if (this.heartbeatSetInterval) { logger.info( - `${this.logPrefix()} Heartbeat already started every ${Utils.formatDurationMilliSeconds( + `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds( this.getHeartbeatInterval() )}` ); } else { logger.error( - `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, not starting the heartbeat` + `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, + not starting the heartbeat` ); } } @@ -540,21 +606,24 @@ export class ChargingStation { } if (!this.getConnectorStatus(connectorId)) { logger.error( - `${this.logPrefix()} Trying to start MeterValues on non existing connector id ${connectorId.toString()}` + `${this.logPrefix()} Trying to start MeterValues on non existing connector id + ${connectorId.toString()}` ); return; } if (this.getConnectorStatus(connectorId)?.transactionStarted === false) { logger.error( - `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started` + `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} + with no transaction started` ); return; } else if ( this.getConnectorStatus(connectorId)?.transactionStarted === true && - Utils.isNullOrUndefined(this.getConnectorStatus(connectorId)?.transactionId) + isNullOrUndefined(this.getConnectorStatus(connectorId)?.transactionId) ) { logger.error( - `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id` + `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} + with no transaction id` ); return; } @@ -606,29 +675,31 @@ export class ChargingStation { if (this.getEnableStatistics() === true) { this.performanceStatistics?.start(); } + if (this.hasFeatureProfile(SupportedFeatureProfiles.Reservation)) { + this.startReservationExpirationSetInterval(); + } this.openWSConnection(); // Monitor charging station template file - this.templateFileWatcher = FileUtils.watchJsonFile( + this.templateFileWatcher = watchJsonFile( this.templateFile, FileType.ChargingStationTemplate, this.logPrefix(), undefined, (event, filename): void => { - if (Utils.isNotEmptyString(filename) && event === 'change') { + if (isNotEmptyString(filename) && event === 'change') { try { logger.debug( `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${ this.templateFile } file have changed, reload` ); - this.sharedLRUCache.deleteChargingStationTemplate(this.stationInfo?.templateHash); + this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash); // Initialize this.initialize(); + this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo)); // Restart the ATG this.stopAutomaticTransactionGenerator(); - if ( - this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true - ) { + if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) { this.startAutomaticTransactionGenerator(); } if (this.getEnableStatistics() === true) { @@ -647,7 +718,7 @@ export class ChargingStation { } ); this.started = true; - parentPort?.postMessage(MessageChannelUtils.buildStartedMessage(this)); + parentPort?.postMessage(buildStartedMessage(this)); this.starting = false; } else { logger.warn(`${this.logPrefix()} Charging station is already starting...`); @@ -668,10 +739,11 @@ export class ChargingStation { } this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash); this.templateFileWatcher?.close(); - this.sharedLRUCache.deleteChargingStationTemplate(this.stationInfo?.templateHash); + this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash); delete this.bootNotificationResponse; this.started = false; - parentPort?.postMessage(MessageChannelUtils.buildStoppedMessage(this)); + this.saveConfiguration(); + parentPort?.postMessage(buildStoppedMessage(this)); this.stopping = false; } else { logger.warn(`${this.logPrefix()} Charging station is already stopping...`); @@ -683,14 +755,14 @@ export class ChargingStation { public async reset(reason?: StopTransactionReason): Promise { await this.stop(reason); - await Utils.sleep(this.stationInfo.resetTime); + await sleep(this.stationInfo.resetTime); this.initialize(); this.start(); } public saveOcppConfiguration(): void { if (this.getOcppPersistentConfiguration()) { - this.saveConfiguration({ stationInfo: false, connectors: false, evses: false }); + this.saveConfiguration(); } } @@ -712,18 +784,18 @@ export class ChargingStation { terminateOpened: false, } ): void { - options.handshakeTimeout = options?.handshakeTimeout ?? this.getConnectionTimeout() * 1000; - params.closeOpened = params?.closeOpened ?? false; - params.terminateOpened = params?.terminateOpened ?? false; + options = { handshakeTimeout: this.getConnectionTimeout() * 1000, ...options }; + params = { ...{ closeOpened: false, terminateOpened: false }, ...params }; if (this.started === false && this.starting === false) { logger.warn( - `${this.logPrefix()} Cannot open OCPP connection to URL ${this.wsConnectionUrl.toString()} on stopped charging station` + `${this.logPrefix()} Cannot open OCPP connection to URL ${this.wsConnectionUrl.toString()} + on stopped charging station` ); return; } if ( - !Utils.isNullOrUndefined(this.stationInfo.supervisionUser) && - !Utils.isNullOrUndefined(this.stationInfo.supervisionPassword) + !isNullOrUndefined(this.stationInfo.supervisionUser) && + !isNullOrUndefined(this.stationInfo.supervisionPassword) ) { options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`; } @@ -736,7 +808,8 @@ export class ChargingStation { if (this.isWebSocketConnectionOpened() === true) { logger.warn( - `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened` + `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} + is already opened` ); return; } @@ -781,34 +854,57 @@ export class ChargingStation { } } - public startAutomaticTransactionGenerator( - connectorIds?: number[], - automaticTransactionGeneratorConfiguration?: AutomaticTransactionGeneratorConfiguration - ): void { - this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance( - automaticTransactionGeneratorConfiguration ?? - this.getAutomaticTransactionGeneratorConfigurationFromTemplate(), - this - ); - if (Utils.isNotEmptyArray(connectorIds)) { + public getAutomaticTransactionGeneratorConfiguration(): + | AutomaticTransactionGeneratorConfiguration + | undefined { + let automaticTransactionGeneratorConfiguration: + | AutomaticTransactionGeneratorConfiguration + | undefined; + const automaticTransactionGeneratorConfigurationFromFile = + this.getConfigurationFromFile()?.automaticTransactionGenerator; + if ( + this.getAutomaticTransactionGeneratorPersistentConfiguration() && + automaticTransactionGeneratorConfigurationFromFile + ) { + automaticTransactionGeneratorConfiguration = + automaticTransactionGeneratorConfigurationFromFile; + } else { + automaticTransactionGeneratorConfiguration = + this.getTemplateFromFile()?.AutomaticTransactionGenerator; + } + return { + ...Constants.DEFAULT_ATG_CONFIGURATION, + ...automaticTransactionGeneratorConfiguration, + }; + } + + public getAutomaticTransactionGeneratorStatuses(): Status[] | undefined { + return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses; + } + + public startAutomaticTransactionGenerator(connectorIds?: number[]): void { + this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this); + if (isNotEmptyArray(connectorIds)) { for (const connectorId of connectorIds) { this.automaticTransactionGenerator?.startConnector(connectorId); } } else { this.automaticTransactionGenerator?.start(); } - parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this)); + this.saveAutomaticTransactionGeneratorConfiguration(); + parentPort?.postMessage(buildUpdatedMessage(this)); } public stopAutomaticTransactionGenerator(connectorIds?: number[]): void { - if (Utils.isNotEmptyArray(connectorIds)) { + if (isNotEmptyArray(connectorIds)) { for (const connectorId of connectorIds) { this.automaticTransactionGenerator?.stopConnector(connectorId); } } else { this.automaticTransactionGenerator?.stop(); } - parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this)); + this.saveAutomaticTransactionGeneratorConfiguration(); + parentPort?.postMessage(buildUpdatedMessage(this)); } public async stopTransactionOnConnector( @@ -848,6 +944,172 @@ export class ChargingStation { ); } + public getReservationOnConnectorId0Enabled(): boolean { + return convertToBoolean( + ChargingStationConfigurationUtils.getConfigurationKey( + this, + StandardParametersKey.ReserveConnectorZeroSupported + ).value + ); + } + + public async addReservation(reservation: Reservation): Promise { + const [exists, reservationFound] = this.doesReservationExists(reservation); + if (exists) { + await this.removeReservation(reservationFound, ReservationTerminationReason.REPLACE_EXISTING); + } + this.getConnectorStatus(reservation.connectorId).reservation = reservation; + await OCPPServiceUtils.sendAndSetConnectorStatus( + this, + reservation.connectorId, + ConnectorStatusEnum.Reserved, + null, + { send: reservation.connectorId !== 0 } + ); + } + + public async removeReservation( + reservation: Reservation, + reason?: ReservationTerminationReason + ): Promise { + const connector = this.getConnectorStatus(reservation.connectorId); + switch (reason) { + case ReservationTerminationReason.CONNECTOR_STATE_CHANGED: + delete connector.reservation; + break; + case ReservationTerminationReason.TRANSACTION_STARTED: + delete connector.reservation; + break; + case ReservationTerminationReason.RESERVATION_CANCELED: + case ReservationTerminationReason.REPLACE_EXISTING: + case ReservationTerminationReason.EXPIRED: + await OCPPServiceUtils.sendAndSetConnectorStatus( + this, + reservation.connectorId, + ConnectorStatusEnum.Available, + null, + { send: reservation.connectorId !== 0 } + ); + delete connector.reservation; + break; + default: + break; + } + } + + public getReservationBy( + filterKey: ReservationFilterKey, + value: number | string + ): Reservation | undefined { + if (this.hasEvses) { + for (const evseStatus of this.evses.values()) { + for (const connectorStatus of evseStatus.connectors.values()) { + if (connectorStatus?.reservation?.[filterKey] === value) { + return connectorStatus.reservation; + } + } + } + } else { + for (const connectorStatus of this.connectors.values()) { + if (connectorStatus?.reservation?.[filterKey] === value) { + return connectorStatus.reservation; + } + } + } + } + + public doesReservationExists(reservation: Partial): [boolean, Reservation] { + const foundReservation = this.getReservationBy( + ReservationFilterKey.RESERVATION_ID, + reservation?.id + ); + return isUndefined(foundReservation) ? [false, null] : [true, foundReservation]; + } + + public startReservationExpirationSetInterval(customInterval?: number): void { + const interval = + customInterval ?? Constants.DEFAULT_RESERVATION_EXPIRATION_OBSERVATION_INTERVAL; + logger.info( + `${this.logPrefix()} Reservation expiration date interval is set to ${interval} + and starts on charging station now` + ); + // eslint-disable-next-line @typescript-eslint/no-misused-promises + this.reservationExpirationSetInterval = setInterval(async (): Promise => { + const now = new Date(); + if (this.hasEvses) { + for (const evseStatus of this.evses.values()) { + for (const connectorStatus of evseStatus.connectors.values()) { + if (connectorStatus?.reservation?.expiryDate < now) { + await this.removeReservation( + connectorStatus.reservation, + ReservationTerminationReason.EXPIRED + ); + } + } + } + } else { + for (const connectorStatus of this.connectors.values()) { + if (connectorStatus?.reservation?.expiryDate < now) { + await this.removeReservation( + connectorStatus.reservation, + ReservationTerminationReason.EXPIRED + ); + } + } + } + }, interval); + } + + public restartReservationExpiryDateSetInterval(): void { + this.stopReservationExpirationSetInterval(); + this.startReservationExpirationSetInterval(); + } + + public validateIncomingRequestWithReservation(connectorId: number, idTag: string): boolean { + return this.getReservationBy(ReservationFilterKey.CONNECTOR_ID, connectorId)?.idTag === idTag; + } + + public isConnectorReservable( + reservationId: number, + idTag?: string, + connectorId?: number + ): boolean { + const [alreadyExists] = this.doesReservationExists({ id: reservationId }); + if (alreadyExists) { + return alreadyExists; + } + const userReservedAlready = isUndefined( + this.getReservationBy(ReservationFilterKey.ID_TAG, idTag) + ) + ? false + : true; + const notConnectorZero = isUndefined(connectorId) ? true : connectorId > 0; + const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0; + return !alreadyExists && !userReservedAlready && notConnectorZero && freeConnectorsAvailable; + } + + private getNumberOfReservableConnectors(): number { + let reservableConnectors = 0; + if (this.hasEvses) { + for (const evseStatus of this.evses.values()) { + reservableConnectors += countReservableConnectors(evseStatus.connectors); + } + } else { + reservableConnectors = countReservableConnectors(this.connectors); + } + return reservableConnectors - this.getNumberOfReservationsOnConnectorZero(); + } + + private getNumberOfReservationsOnConnectorZero(): number { + let numberOfReservations = 0; + if (this.hasEvses && this.evses.get(0)?.connectors.get(0)?.reservation) { + ++numberOfReservations; + } else if (this.connectors.get(0)?.reservation) { + ++numberOfReservations; + } + return numberOfReservations; + } + private flushMessageBuffer(): void { if (this.messageBuffer.size > 0) { for (const message of this.messageBuffer.values()) { @@ -875,6 +1137,12 @@ export class ChargingStation { return this.stationInfo.supervisionUrlOcppConfiguration ?? false; } + private stopReservationExpirationSetInterval(): void { + if (this.reservationExpirationSetInterval) { + clearInterval(this.reservationExpirationSetInterval); + } + } + private getSupervisionUrlOcppKey(): string { return this.stationInfo.supervisionUrlOcppKey ?? VendorParametersKey.ConnectionUrl; } @@ -882,23 +1150,21 @@ export class ChargingStation { private getTemplateFromFile(): ChargingStationTemplate | undefined { let template: ChargingStationTemplate; try { - if (this.sharedLRUCache.hasChargingStationTemplate(this.stationInfo?.templateHash)) { - template = this.sharedLRUCache.getChargingStationTemplate(this.stationInfo.templateHash); + if (this.sharedLRUCache.hasChargingStationTemplate(this.templateFileHash)) { + template = this.sharedLRUCache.getChargingStationTemplate(this.templateFileHash); } else { const measureId = `${FileType.ChargingStationTemplate} read`; const beginId = PerformanceStatistics.beginMeasure(measureId); - template = JSON.parse( - fs.readFileSync(this.templateFile, 'utf8') - ) as ChargingStationTemplate; + template = JSON.parse(readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate; PerformanceStatistics.endMeasure(measureId, beginId); - template.templateHash = crypto - .createHash(Constants.DEFAULT_HASH_ALGORITHM) + template.templateHash = createHash(Constants.DEFAULT_HASH_ALGORITHM) .update(JSON.stringify(template)) .digest('hex'); this.sharedLRUCache.setChargingStationTemplate(template); + this.templateFileHash = template.templateHash; } } catch (error) { - FileUtils.handleFileException( + handleFileException( this.templateFile, FileType.ChargingStationTemplate, error as NodeJS.ErrnoException, @@ -910,33 +1176,19 @@ export class ChargingStation { private getStationInfoFromTemplate(): ChargingStationInfo { const stationTemplate: ChargingStationTemplate | undefined = this.getTemplateFromFile(); - if (Utils.isNullOrUndefined(stationTemplate)) { - const errorMsg = `Failed to read charging station template file ${this.templateFile}`; - logger.error(`${this.logPrefix()} ${errorMsg}`); - throw new BaseError(errorMsg); - } - if (Utils.isEmptyObject(stationTemplate)) { - const errorMsg = `Empty charging station information from template file ${this.templateFile}`; - logger.error(`${this.logPrefix()} ${errorMsg}`); - throw new BaseError(errorMsg); - } - ChargingStationUtils.warnTemplateKeysDeprecation( - this.templateFile, - stationTemplate, - this.logPrefix() - ); - const stationInfo: ChargingStationInfo = - ChargingStationUtils.stationTemplateToStationInfo(stationTemplate); - stationInfo.hashId = ChargingStationUtils.getHashId(this.index, stationTemplate); - stationInfo.chargingStationId = ChargingStationUtils.getChargingStationId( - this.index, - stationTemplate - ); + checkTemplate(stationTemplate, this.logPrefix(), this.templateFile); + warnTemplateKeysDeprecation(stationTemplate, this.logPrefix(), this.templateFile); + if (stationTemplate?.Connectors) { + checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile); + } + const stationInfo: ChargingStationInfo = stationTemplateToStationInfo(stationTemplate); + stationInfo.hashId = getHashId(this.index, stationTemplate); + stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate); stationInfo.ocppVersion = stationTemplate?.ocppVersion ?? OCPPVersion.VERSION_16; - ChargingStationUtils.createSerialNumber(stationTemplate, stationInfo); - if (Utils.isNotEmptyArray(stationTemplate?.power)) { + createSerialNumber(stationTemplate, stationInfo); + if (isNotEmptyArray(stationTemplate?.power)) { stationTemplate.power = stationTemplate.power as number[]; - const powerArrayRandomIndex = Math.floor(Utils.secureRandom() * stationTemplate.power.length); + const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length); stationInfo.maximumPower = stationTemplate?.powerUnit === PowerUnits.KILO_WATT ? stationTemplate.power[powerArrayRandomIndex] * 1000 @@ -951,7 +1203,7 @@ export class ChargingStation { stationInfo.firmwareVersionPattern = stationTemplate?.firmwareVersionPattern ?? Constants.SEMVER_PATTERN; if ( - Utils.isNotEmptyString(stationInfo.firmwareVersion) && + isNotEmptyString(stationInfo.firmwareVersion) && new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion) === false ) { logger.warn( @@ -969,21 +1221,21 @@ export class ChargingStation { }, stationTemplate?.firmwareUpgrade ?? {} ); - stationInfo.resetTime = !Utils.isNullOrUndefined(stationTemplate?.resetTime) + stationInfo.resetTime = !isNullOrUndefined(stationTemplate?.resetTime) ? stationTemplate.resetTime * 1000 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME; - // Initialize evses or connectors if needed (FIXME: should be factored out) - this.initializeConnectorsOrEvses(stationInfo); stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo); - ChargingStationUtils.createStationInfoHash(stationInfo); return stationInfo; } private getStationInfoFromFile(): ChargingStationInfo | undefined { let stationInfo: ChargingStationInfo | undefined; - this.getStationInfoPersistentConfiguration() && - (stationInfo = this.getConfigurationFromFile()?.stationInfo); - stationInfo && ChargingStationUtils.createStationInfoHash(stationInfo); + if (this.getStationInfoPersistentConfiguration()) { + stationInfo = this.getConfigurationFromFile()?.stationInfo; + if (stationInfo) { + delete stationInfo?.infoHash; + } + } return stationInfo; } @@ -993,15 +1245,11 @@ export class ChargingStation { // Priority: // 1. charging station info from template // 2. charging station info from configuration file - // 3. charging station info attribute if (stationInfoFromFile?.templateHash === stationInfoFromTemplate.templateHash) { - if (this.stationInfo?.infoHash === stationInfoFromFile?.infoHash) { - return this.stationInfo; - } return stationInfoFromFile; } stationInfoFromFile && - ChargingStationUtils.propagateSerialNumber( + propagateSerialNumber( this.getTemplateFromFile(), stationInfoFromFile, stationInfoFromTemplate @@ -1011,7 +1259,7 @@ export class ChargingStation { private saveStationInfo(): void { if (this.getStationInfoPersistentConfiguration()) { - this.saveConfiguration({ ocppConfiguration: false, connectors: false, evses: false }); + this.saveConfiguration(); } } @@ -1023,22 +1271,38 @@ export class ChargingStation { return this.stationInfo?.stationInfoPersistentConfiguration ?? true; } + private getAutomaticTransactionGeneratorPersistentConfiguration(): boolean { + return this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration ?? true; + } + private handleUnsupportedVersion(version: OCPPVersion) { - const errMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`; - logger.error(`${this.logPrefix()} ${errMsg}`); - throw new BaseError(errMsg); + const errorMsg = `Unsupported protocol version '${version}' configured + in template file ${this.templateFile}`; + logger.error(`${this.logPrefix()} ${errorMsg}`); + throw new BaseError(errorMsg); } private initialize(): void { - this.configurationFile = path.join( - path.dirname(this.templateFile.replace('station-templates', 'configurations')), - `${ChargingStationUtils.getHashId(this.index, this.getTemplateFromFile())}.json` + const stationTemplate = this.getTemplateFromFile(); + checkTemplate(stationTemplate, this.logPrefix(), this.templateFile); + this.configurationFile = join( + dirname(this.templateFile.replace('station-templates', 'configurations')), + `${getHashId(this.index, stationTemplate)}.json` ); + const chargingStationConfiguration = this.getConfigurationFromFile(); + if ( + chargingStationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash && + (chargingStationConfiguration?.connectorsStatus || chargingStationConfiguration?.evsesStatus) + ) { + this.initializeConnectorsOrEvsesFromFile(chargingStationConfiguration); + } else { + this.initializeConnectorsOrEvsesFromTemplate(stationTemplate); + } this.stationInfo = this.getStationInfo(); if ( this.stationInfo.firmwareStatus === FirmwareStatus.Installing && - Utils.isNotEmptyString(this.stationInfo.firmwareVersion) && - Utils.isNotEmptyString(this.stationInfo.firmwareVersionPattern) + isNotEmptyString(this.stationInfo.firmwareVersion) && + isNotEmptyString(this.stationInfo.firmwareVersionPattern) ) { const patternGroup: number | undefined = this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ?? @@ -1048,15 +1312,12 @@ export class ChargingStation { ?.slice(1, patternGroup + 1); const patchLevelIndex = match.length - 1; match[patchLevelIndex] = ( - Utils.convertToInt(match[patchLevelIndex]) + + convertToInt(match[patchLevelIndex]) + this.stationInfo.firmwareUpgrade?.versionUpgrade?.step ).toString(); this.stationInfo.firmwareVersion = match?.join('.'); } this.saveStationInfo(); - // Avoid duplication of connectors or evses related information in RAM - delete this.stationInfo?.Connectors; - delete this.stationInfo?.Evses; this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl(); if (this.getEnableStatistics() === true) { this.performanceStatistics = PerformanceStatistics.getInstance( @@ -1065,9 +1326,7 @@ export class ChargingStation { this.configuredSupervisionUrl ); } - this.bootNotificationRequest = ChargingStationUtils.createBootNotificationRequest( - this.stationInfo - ); + this.bootNotificationRequest = createBootNotificationRequest(this.stationInfo); this.powerDivider = this.getPowerDivider(); // OCPP configuration this.ocppConfiguration = this.getOcppConfiguration(); @@ -1134,7 +1393,7 @@ export class ChargingStation { } if ( this.getSupervisionUrlOcppConfiguration() && - Utils.isNotEmptyString(this.getSupervisionUrlOcppKey()) && + isNotEmptyString(this.getSupervisionUrlOcppKey()) && !ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey()) ) { ChargingStationConfigurationUtils.addConfigurationKey( @@ -1145,7 +1404,7 @@ export class ChargingStation { ); } else if ( !this.getSupervisionUrlOcppConfiguration() && - Utils.isNotEmptyString(this.getSupervisionUrlOcppKey()) && + isNotEmptyString(this.getSupervisionUrlOcppKey()) && ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey()) ) { ChargingStationConfigurationUtils.deleteConfigurationKey( @@ -1155,7 +1414,7 @@ export class ChargingStation { ); } if ( - Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) && + isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) && !ChargingStationConfigurationUtils.getConfigurationKey( this, this.stationInfo.amperageLimitationOcppKey @@ -1165,8 +1424,7 @@ export class ChargingStation { this, this.stationInfo.amperageLimitationOcppKey, ( - this.stationInfo.maximumAmperage * - ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo) + this.stationInfo.maximumAmperage * getAmperageLimitationUnitDivider(this.stationInfo) ).toString() ); } @@ -1212,14 +1470,14 @@ export class ChargingStation { for (const evseStatus of this.evses.values()) { for (const connectorId of evseStatus.connectors.keys()) { connectorsPhaseRotation.push( - ChargingStationUtils.getPhaseRotationValue(connectorId, this.getNumberOfPhases()) + getPhaseRotationValue(connectorId, this.getNumberOfPhases()) ); } } } else { for (const connectorId of this.connectors.keys()) { connectorsPhaseRotation.push( - ChargingStationUtils.getPhaseRotationValue(connectorId, this.getNumberOfPhases()) + getPhaseRotationValue(connectorId, this.getNumberOfPhases()) ); } } @@ -1272,12 +1530,42 @@ export class ChargingStation { this.saveOcppConfiguration(); } - private initializeConnectorsOrEvses(stationInfo: ChargingStationInfo) { - if (stationInfo?.Connectors && !stationInfo?.Evses) { - this.initializeConnectors(stationInfo); - } else if (stationInfo?.Evses && !stationInfo?.Connectors) { - this.initializeEvses(stationInfo); - } else if (stationInfo?.Evses && stationInfo?.Connectors) { + private initializeConnectorsOrEvsesFromFile(configuration: ChargingStationConfiguration): void { + if (configuration?.connectorsStatus && !configuration?.evsesStatus) { + for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) { + this.connectors.set(connectorId, cloneObject(connectorStatus)); + } + } else if (configuration?.evsesStatus && !configuration?.connectorsStatus) { + for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) { + const evseStatus = cloneObject(evseStatusConfiguration); + delete evseStatus.connectorsStatus; + this.evses.set(evseId, { + ...(evseStatus as EvseStatus), + connectors: new Map( + evseStatusConfiguration.connectorsStatus.map((connectorStatus, connectorId) => [ + connectorId, + connectorStatus, + ]) + ), + }); + } + } else if (configuration?.evsesStatus && configuration?.connectorsStatus) { + const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`; + logger.error(`${this.logPrefix()} ${errorMsg}`); + throw new BaseError(errorMsg); + } else { + const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}`; + logger.error(`${this.logPrefix()} ${errorMsg}`); + throw new BaseError(errorMsg); + } + } + + private initializeConnectorsOrEvsesFromTemplate(stationTemplate: ChargingStationTemplate) { + if (stationTemplate?.Connectors && !stationTemplate?.Evses) { + this.initializeConnectorsFromTemplate(stationTemplate); + } else if (stationTemplate?.Evses && !stationTemplate?.Connectors) { + this.initializeEvsesFromTemplate(stationTemplate); + } else if (stationTemplate?.Evses && stationTemplate?.Connectors) { const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`; logger.error(`${this.logPrefix()} ${errorMsg}`); throw new BaseError(errorMsg); @@ -1288,81 +1576,55 @@ export class ChargingStation { } } - private initializeConnectors(stationInfo: ChargingStationInfo): void { - if (!stationInfo?.Connectors && this.connectors.size === 0) { - const logMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`; - logger.error(`${this.logPrefix()} ${logMsg}`); - throw new BaseError(logMsg); + private initializeConnectorsFromTemplate(stationTemplate: ChargingStationTemplate): void { + if (!stationTemplate?.Connectors && this.connectors.size === 0) { + const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`; + logger.error(`${this.logPrefix()} ${errorMsg}`); + throw new BaseError(errorMsg); } - if (!stationInfo?.Connectors[0]) { + if (!stationTemplate?.Connectors[0]) { logger.warn( `${this.logPrefix()} Charging station information from template ${ this.templateFile } with no connector id 0 configuration` ); } - if (stationInfo?.Connectors) { - const configuredMaxConnectors = - ChargingStationUtils.getConfiguredNumberOfConnectors(stationInfo); - ChargingStationUtils.checkConfiguredMaxConnectors( - configuredMaxConnectors, - this.templateFile, - this.logPrefix() - ); - const connectorsConfigHash = crypto - .createHash(Constants.DEFAULT_HASH_ALGORITHM) - .update(`${JSON.stringify(stationInfo?.Connectors)}${configuredMaxConnectors.toString()}`) + if (stationTemplate?.Connectors) { + const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } = + checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile); + const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM) + .update( + `${JSON.stringify(stationTemplate?.Connectors)}${configuredMaxConnectors.toString()}` + ) .digest('hex'); const connectorsConfigChanged = this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash; if (this.connectors?.size === 0 || connectorsConfigChanged) { connectorsConfigChanged && this.connectors.clear(); this.connectorsConfigurationHash = connectorsConfigHash; - const templateMaxConnectors = ChargingStationUtils.getMaxNumberOfConnectors( - stationInfo.Connectors - ); - ChargingStationUtils.checkTemplateMaxConnectors( - templateMaxConnectors, - this.templateFile, - this.logPrefix() - ); - const templateMaxAvailableConnectors = stationInfo?.Connectors[0] - ? templateMaxConnectors - 1 - : templateMaxConnectors; - if ( - configuredMaxConnectors > templateMaxAvailableConnectors && - !stationInfo?.randomConnectors - ) { - logger.warn( - `${this.logPrefix()} Number of connectors exceeds the number of connector configurations in template ${ - this.templateFile - }, forcing random connector configurations affectation` - ); - stationInfo.randomConnectors = true; - } if (templateMaxConnectors > 0) { for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) { if ( connectorId === 0 && - (!stationInfo?.Connectors[connectorId] || - this.getUseConnectorId0(stationInfo) === false) + (!stationTemplate?.Connectors[connectorId] || + this.getUseConnectorId0(stationTemplate) === false) ) { continue; } const templateConnectorId = - connectorId > 0 && stationInfo?.randomConnectors - ? Utils.getRandomInteger(templateMaxAvailableConnectors, 1) + connectorId > 0 && stationTemplate?.randomConnectors + ? getRandomInteger(templateMaxAvailableConnectors, 1) : connectorId; - const connectorStatus = stationInfo?.Connectors[templateConnectorId]; - ChargingStationUtils.checkStationInfoConnectorStatus( + const connectorStatus = stationTemplate?.Connectors[templateConnectorId]; + checkStationInfoConnectorStatus( templateConnectorId, connectorStatus, this.logPrefix(), this.templateFile ); - this.connectors.set(connectorId, Utils.cloneObject(connectorStatus)); + this.connectors.set(connectorId, cloneObject(connectorStatus)); } - ChargingStationUtils.initializeConnectorsMapStatus(this.connectors, this.logPrefix()); + initializeConnectorsMapStatus(this.connectors, this.logPrefix()); this.saveConnectorsStatus(); } else { logger.warn( @@ -1381,52 +1643,48 @@ export class ChargingStation { } } - private initializeEvses(stationInfo: ChargingStationInfo): void { - if (!stationInfo?.Evses && this.evses.size === 0) { - const logMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`; - logger.error(`${this.logPrefix()} ${logMsg}`); - throw new BaseError(logMsg); + private initializeEvsesFromTemplate(stationTemplate: ChargingStationTemplate): void { + if (!stationTemplate?.Evses && this.evses.size === 0) { + const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`; + logger.error(`${this.logPrefix()} ${errorMsg}`); + throw new BaseError(errorMsg); } - if (!stationInfo?.Evses[0]) { + if (!stationTemplate?.Evses[0]) { logger.warn( `${this.logPrefix()} Charging station information from template ${ this.templateFile } with no evse id 0 configuration` ); } - if (!stationInfo?.Evses[0]?.Connectors[0]) { + if (!stationTemplate?.Evses[0]?.Connectors[0]) { logger.warn( `${this.logPrefix()} Charging station information from template ${ this.templateFile } with evse id 0 with no connector id 0 configuration` ); } - if (stationInfo?.Evses) { - const evsesConfigHash = crypto - .createHash(Constants.DEFAULT_HASH_ALGORITHM) - .update(`${JSON.stringify(stationInfo?.Evses)}`) + if (stationTemplate?.Evses) { + const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM) + .update(JSON.stringify(stationTemplate?.Evses)) .digest('hex'); const evsesConfigChanged = this.evses?.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash; if (this.evses?.size === 0 || evsesConfigChanged) { evsesConfigChanged && this.evses.clear(); this.evsesConfigurationHash = evsesConfigHash; - const templateMaxEvses = ChargingStationUtils.getMaxNumberOfEvses(stationInfo?.Evses); + const templateMaxEvses = getMaxNumberOfEvses(stationTemplate?.Evses); if (templateMaxEvses > 0) { - for (const evse in stationInfo.Evses) { - const evseId = Utils.convertToInt(evse); + for (const evse in stationTemplate.Evses) { + const evseId = convertToInt(evse); this.evses.set(evseId, { - connectors: ChargingStationUtils.buildConnectorsMap( - stationInfo?.Evses[evse]?.Connectors, + connectors: buildConnectorsMap( + stationTemplate?.Evses[evse]?.Connectors, this.logPrefix(), this.templateFile ), availability: AvailabilityType.Operative, }); - ChargingStationUtils.initializeConnectorsMapStatus( - this.evses.get(evseId)?.connectors, - this.logPrefix() - ); + initializeConnectorsMapStatus(this.evses.get(evseId)?.connectors, this.logPrefix()); } this.saveEvsesStatus(); } else { @@ -1448,7 +1706,7 @@ export class ChargingStation { private getConfigurationFromFile(): ChargingStationConfiguration | undefined { let configuration: ChargingStationConfiguration | undefined; - if (this.configurationFile && fs.existsSync(this.configurationFile)) { + if (isNotEmptyString(this.configurationFile) && existsSync(this.configurationFile)) { try { if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) { configuration = this.sharedLRUCache.getChargingStationConfiguration( @@ -1458,14 +1716,14 @@ export class ChargingStation { const measureId = `${FileType.ChargingStationConfiguration} read`; const beginId = PerformanceStatistics.beginMeasure(measureId); configuration = JSON.parse( - fs.readFileSync(this.configurationFile, 'utf8') + readFileSync(this.configurationFile, 'utf8') ) as ChargingStationConfiguration; PerformanceStatistics.endMeasure(measureId, beginId); - this.configurationFileHash = configuration.configurationHash; this.sharedLRUCache.setChargingStationConfiguration(configuration); + this.configurationFileHash = configuration.configurationHash; } } catch (error) { - FileUtils.handleFileException( + handleFileException( this.configurationFile, FileType.ChargingStationConfiguration, error as NodeJS.ErrnoException, @@ -1476,78 +1734,93 @@ export class ChargingStation { return configuration; } - private saveConnectorsStatus() { - if (this.getOcppPersistentConfiguration()) { - this.saveConfiguration({ stationInfo: false, ocppConfiguration: false, evses: false }); + private saveAutomaticTransactionGeneratorConfiguration(): void { + if (this.getAutomaticTransactionGeneratorPersistentConfiguration()) { + this.saveConfiguration(); } } + private saveConnectorsStatus() { + this.saveConfiguration(); + } + private saveEvsesStatus() { - if (this.getOcppPersistentConfiguration()) { - this.saveConfiguration({ stationInfo: false, ocppConfiguration: false, connectors: false }); - } + this.saveConfiguration(); } - private saveConfiguration( - params: { - stationInfo?: boolean; - ocppConfiguration?: boolean; - connectors?: boolean; - evses?: boolean; - } = { stationInfo: true, ocppConfiguration: true, connectors: true, evses: true } - ): void { - if (this.configurationFile) { - params = { - ...params, - ...{ stationInfo: true, ocppConfiguration: true, connectors: true, evses: true }, - }; + private saveConfiguration(): void { + if (isNotEmptyString(this.configurationFile)) { try { - if (!fs.existsSync(path.dirname(this.configurationFile))) { - fs.mkdirSync(path.dirname(this.configurationFile), { recursive: true }); + if (!existsSync(dirname(this.configurationFile))) { + mkdirSync(dirname(this.configurationFile), { recursive: true }); } - const configurationData: ChargingStationConfiguration = - Utils.cloneObject(this.getConfigurationFromFile()) ?? {}; - if (params.stationInfo && this.stationInfo) { + let configurationData: ChargingStationConfiguration = + cloneObject(this.getConfigurationFromFile()) ?? {}; + if (this.getStationInfoPersistentConfiguration() && this.stationInfo) { configurationData.stationInfo = this.stationInfo; + } else { + delete configurationData.stationInfo; } - if (params.ocppConfiguration && this.ocppConfiguration?.configurationKey) { + if (this.getOcppPersistentConfiguration() && this.ocppConfiguration?.configurationKey) { configurationData.configurationKey = this.ocppConfiguration.configurationKey; + } else { + delete configurationData.configurationKey; } - if (params.connectors && this.connectors.size > 0) { - configurationData.connectorsStatus = [...this.connectors.values()].map( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ({ transactionSetInterval, ...connectorStatusRest }) => connectorStatusRest - ); + configurationData = merge( + configurationData, + buildChargingStationAutomaticTransactionGeneratorConfiguration(this) + ); + if ( + !this.getAutomaticTransactionGeneratorPersistentConfiguration() || + !this.getAutomaticTransactionGeneratorConfiguration() + ) { + delete configurationData.automaticTransactionGenerator; } - if (params.evses && this.evses.size > 0) { - configurationData.evsesStatus = [...this.evses.values()].map((evseStatus) => { - const status = { - ...evseStatus, - connectorsStatus: [...evseStatus.connectors.values()].map( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ({ transactionSetInterval, ...connectorStatusRest }) => connectorStatusRest - ), - }; - delete status.connectors; - return status as EvseStatusConfiguration; - }); + if (this.connectors.size > 0) { + configurationData.connectorsStatus = buildConnectorsStatus(this); + } else { + delete configurationData.connectorsStatus; + } + if (this.evses.size > 0) { + configurationData.evsesStatus = buildEvsesStatus(this); + } else { + delete configurationData.evsesStatus; } delete configurationData.configurationHash; - const configurationHash = crypto - .createHash(Constants.DEFAULT_HASH_ALGORITHM) - .update(JSON.stringify(configurationData)) + const configurationHash = createHash(Constants.DEFAULT_HASH_ALGORITHM) + .update( + JSON.stringify({ + stationInfo: configurationData.stationInfo, + configurationKey: configurationData.configurationKey, + automaticTransactionGenerator: configurationData.automaticTransactionGenerator, + } as ChargingStationConfiguration) + ) .digest('hex'); if (this.configurationFileHash !== configurationHash) { - configurationData.configurationHash = configurationHash; - const measureId = `${FileType.ChargingStationConfiguration} write`; - const beginId = PerformanceStatistics.beginMeasure(measureId); - const fileDescriptor = fs.openSync(this.configurationFile, 'w'); - fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8'); - fs.closeSync(fileDescriptor); - PerformanceStatistics.endMeasure(measureId, beginId); - this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash); - this.configurationFileHash = configurationHash; - this.sharedLRUCache.setChargingStationConfiguration(configurationData); + AsyncLock.acquire(AsyncLockType.configuration) + .then(() => { + configurationData.configurationHash = configurationHash; + const measureId = `${FileType.ChargingStationConfiguration} write`; + const beginId = PerformanceStatistics.beginMeasure(measureId); + const fileDescriptor = openSync(this.configurationFile, 'w'); + writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8'); + closeSync(fileDescriptor); + PerformanceStatistics.endMeasure(measureId, beginId); + this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash); + this.sharedLRUCache.setChargingStationConfiguration(configurationData); + this.configurationFileHash = configurationHash; + }) + .catch((error) => { + handleFileException( + this.configurationFile, + FileType.ChargingStationConfiguration, + error as NodeJS.ErrnoException, + this.logPrefix() + ); + }) + .finally(() => { + AsyncLock.release(AsyncLockType.configuration).catch(Constants.EMPTY_FUNCTION); + }); } else { logger.debug( `${this.logPrefix()} Not saving unchanged charging station configuration file ${ @@ -1556,7 +1829,7 @@ export class ChargingStation { ); } } catch (error) { - FileUtils.handleFileException( + handleFileException( this.configurationFile, FileType.ChargingStationConfiguration, error as NodeJS.ErrnoException, @@ -1575,16 +1848,11 @@ export class ChargingStation { } private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined { - let configuration: ChargingStationConfiguration | undefined; - if (this.getOcppPersistentConfiguration() === true) { - const configurationFromFile = this.getConfigurationFromFile(); - configuration = configurationFromFile?.configurationKey && configurationFromFile; - } - if (!Utils.isNullOrUndefined(configuration)) { - delete configuration.stationInfo; - delete configuration.configurationHash; + const configurationKey = this.getConfigurationFromFile()?.configurationKey; + if (this.getOcppPersistentConfiguration() === true && configurationKey) { + return { configurationKey }; } - return configuration; + return undefined; } private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined { @@ -1612,11 +1880,11 @@ export class ChargingStation { skipBufferingOnError: true, }); if (this.isRegistered() === false) { - this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++; - await Utils.sleep( + this.getRegistrationMaxRetries() !== -1 && ++registrationRetryCount; + await sleep( this?.bootNotificationResponse?.interval ? this.bootNotificationResponse.interval * 1000 - : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL + : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL ); } } while ( @@ -1626,7 +1894,7 @@ export class ChargingStation { ); } if (this.isRegistered() === true) { - if (this.isInAcceptedState() === true) { + if (this.inAcceptedState() === true) { await this.startMessageSequence(); } } else { @@ -1636,7 +1904,7 @@ export class ChargingStation { } this.wsConnectionRestarted = false; this.autoReconnectRetryCount = 0; - parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this)); + parentPort?.postMessage(buildUpdatedMessage(this)); } else { logger.warn( `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed` @@ -1650,7 +1918,7 @@ export class ChargingStation { case WebSocketCloseEventStatusCode.CLOSE_NORMAL: case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS: logger.info( - `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString( + `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString( code )}' and reason '${reason.toString()}'` ); @@ -1659,14 +1927,14 @@ export class ChargingStation { // Abnormal close default: logger.error( - `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString( + `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString( code )}' and reason '${reason.toString()}'` ); this.started === true && (await this.reconnect()); break; } - parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this)); + parentPort?.postMessage(buildUpdatedMessage(this)); } private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined { @@ -1750,7 +2018,7 @@ export class ChargingStation { private async onMessage(data: RawData): Promise { let request: IncomingRequest | Response | ErrorResponse; let messageType: number; - let errMsg: string; + let errorMsg: string; try { request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse; if (Array.isArray(request) === true) { @@ -1772,11 +2040,11 @@ export class ChargingStation { // Unknown Message default: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - errMsg = `Wrong message type ${messageType}`; - logger.error(`${this.logPrefix()} ${errMsg}`); - throw new OCPPError(ErrorType.PROTOCOL_ERROR, errMsg); + errorMsg = `Wrong message type ${messageType}`; + logger.error(`${this.logPrefix()} ${errorMsg}`); + throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg); } - parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this)); + parentPort?.postMessage(buildUpdatedMessage(this)); } else { throw new OCPPError(ErrorType.PROTOCOL_ERROR, 'Incoming message is not an array', null, { request, @@ -1854,34 +2122,16 @@ export class ChargingStation { ); } - private getUseConnectorId0(stationInfo?: ChargingStationInfo): boolean { - const localStationInfo = stationInfo ?? this.stationInfo; - return localStationInfo?.useConnectorId0 ?? true; - } - - private getNumberOfRunningTransactions(): number { - let trxCount = 0; - if (this.hasEvses) { - for (const evseStatus of this.evses.values()) { - for (const connectorStatus of evseStatus.connectors.values()) { - if (connectorStatus.transactionStarted === true) { - trxCount++; - } - } - } - } else { - for (const connectorId of this.connectors.keys()) { - if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) { - trxCount++; - } - } - } - return trxCount; + private getUseConnectorId0(stationTemplate?: ChargingStationTemplate): boolean { + return stationTemplate?.useConnectorId0 ?? true; } private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise { if (this.hasEvses) { - for (const evseStatus of this.evses.values()) { + for (const [evseId, evseStatus] of this.evses) { + if (evseId === 0) { + continue; + } for (const [connectorId, connectorStatus] of evseStatus.connectors) { if (connectorStatus.transactionStarted === true) { await this.stopTransactionOnConnector(connectorId, reason); @@ -1919,25 +2169,18 @@ export class ChargingStation { // -1 for unlimited, 0 for disabling private getAutoReconnectMaxRetries(): number | undefined { - if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) { - return this.stationInfo.autoReconnectMaxRetries; - } - if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) { - return Configuration.getAutoReconnectMaxRetries(); - } - return -1; + return ( + this.stationInfo.autoReconnectMaxRetries ?? Configuration.getAutoReconnectMaxRetries() ?? -1 + ); } // 0 for disabling private getRegistrationMaxRetries(): number | undefined { - if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) { - return this.stationInfo.registrationMaxRetries; - } - return -1; + return this.stationInfo.registrationMaxRetries ?? -1; } private getPowerDivider(): number { - let powerDivider = this.getNumberOfConnectors(); + let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors(); if (this.stationInfo?.powerSharedByConnectors) { powerDivider = this.getNumberOfRunningTransactions(); } @@ -1950,7 +2193,7 @@ export class ChargingStation { case CurrentType.AC: return ACElectricUtils.amperagePerPhaseFromPower( this.getNumberOfPhases(stationInfo), - maximumPower / this.getNumberOfConnectors(), + maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()), this.getVoltageOut(stationInfo) ); case CurrentType.DC: @@ -1960,19 +2203,19 @@ export class ChargingStation { private getAmperageLimitation(): number | undefined { if ( - Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) && + isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) && ChargingStationConfigurationUtils.getConfigurationKey( this, this.stationInfo.amperageLimitationOcppKey ) ) { return ( - Utils.convertToInt( + convertToInt( ChargingStationConfigurationUtils.getConfigurationKey( this, this.stationInfo.amperageLimitationOcppKey )?.value - ) / ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo) + ) / getAmperageLimitationUnitDivider(this.stationInfo) ); } } @@ -1995,15 +2238,12 @@ export class ChargingStation { for (const [evseId, evseStatus] of this.evses) { if (evseId > 0) { for (const [connectorId, connectorStatus] of evseStatus.connectors) { - const connectorBootStatus = ChargingStationUtils.getBootConnectorStatus( - this, - connectorId, - connectorStatus - ); + const connectorBootStatus = getBootConnectorStatus(this, connectorId, connectorStatus); await OCPPServiceUtils.sendAndSetConnectorStatus( this, connectorId, - connectorBootStatus + connectorBootStatus, + evseId ); } } @@ -2011,7 +2251,7 @@ export class ChargingStation { } else { for (const connectorId of this.connectors.keys()) { if (connectorId > 0) { - const connectorBootStatus = ChargingStationUtils.getBootConnectorStatus( + const connectorBootStatus = getBootConnectorStatus( this, connectorId, this.getConnectorStatus(connectorId) @@ -2031,7 +2271,7 @@ export class ChargingStation { } // Start the ATG - if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true) { + if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) { this.startAutomaticTransactionGenerator(); } this.wsConnectionRestarted === true && this.flushMessageBuffer(); @@ -2063,7 +2303,8 @@ export class ChargingStation { OCPPServiceUtils.buildStatusNotificationRequest( this, connectorId, - ConnectorStatusEnum.Unavailable + ConnectorStatusEnum.Unavailable, + evseId ) ); delete connectorStatus?.status; @@ -2096,7 +2337,7 @@ export class ChargingStation { this, StandardParametersKey.WebSocketPingInterval ) - ? Utils.convertToInt( + ? convertToInt( ChargingStationConfigurationUtils.getConfigurationKey( this, StandardParametersKey.WebSocketPingInterval @@ -2110,13 +2351,13 @@ export class ChargingStation { } }, webSocketPingInterval * 1000); logger.info( - `${this.logPrefix()} WebSocket ping started every ${Utils.formatDurationSeconds( + `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds( webSocketPingInterval )}` ); } else if (this.webSocketPingSetInterval) { logger.info( - `${this.logPrefix()} WebSocket ping already started every ${Utils.formatDurationSeconds( + `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds( webSocketPingInterval )}` ); @@ -2135,12 +2376,13 @@ export class ChargingStation { } private getConfiguredSupervisionUrl(): URL { + let configuredSupervisionUrl: string; const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls(); - if (Utils.isNotEmptyArray(supervisionUrls)) { + if (isNotEmptyArray(supervisionUrls)) { let configuredSupervisionUrlIndex: number; switch (Configuration.getSupervisionUrlDistribution()) { case SupervisionUrlDistribution.RANDOM: - configuredSupervisionUrlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length); + configuredSupervisionUrlIndex = Math.floor(secureRandom() * supervisionUrls.length); break; case SupervisionUrlDistribution.ROUND_ROBIN: case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY: @@ -2156,9 +2398,16 @@ export class ChargingStation { configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length; break; } - return new URL(supervisionUrls[configuredSupervisionUrlIndex]); + configuredSupervisionUrl = supervisionUrls[configuredSupervisionUrlIndex]; + } else { + configuredSupervisionUrl = supervisionUrls as string; } - return new URL(supervisionUrls as string); + if (isNotEmptyString(configuredSupervisionUrl)) { + return new URL(configuredSupervisionUrl); + } + const errorMsg = 'No supervision url(s) configured'; + logger.error(`${this.logPrefix()} ${errorMsg}`); + throw new BaseError(`${errorMsg}`); } private stopHeartbeat(): void { @@ -2185,16 +2434,16 @@ export class ChargingStation { // Stop heartbeat this.stopHeartbeat(); // Stop the ATG if needed - if (this.automaticTransactionGenerator?.configuration?.stopOnConnectionFailure === true) { + if (this.getAutomaticTransactionGeneratorConfiguration().stopOnConnectionFailure === true) { this.stopAutomaticTransactionGenerator(); } if ( this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() || this.getAutoReconnectMaxRetries() === -1 ) { - this.autoReconnectRetryCount++; + ++this.autoReconnectRetryCount; const reconnectDelay = this.getReconnectExponentialDelay() - ? Utils.exponentialDelay(this.autoReconnectRetryCount) + ? exponentialDelay(this.autoReconnectRetryCount) : this.getConnectionTimeout() * 1000; const reconnectDelayWithdraw = 1000; const reconnectTimeout = @@ -2202,12 +2451,12 @@ export class ChargingStation { ? reconnectDelay - reconnectDelayWithdraw : 0; logger.error( - `${this.logPrefix()} WebSocket connection retry in ${Utils.roundTo( + `${this.logPrefix()} WebSocket connection retry in ${roundTo( reconnectDelay, 2 )}ms, timeout ${reconnectTimeout}ms` ); - await Utils.sleep(reconnectDelay); + await sleep(reconnectDelay); logger.error( `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}` ); @@ -2227,10 +2476,4 @@ export class ChargingStation { ); } } - - private getAutomaticTransactionGeneratorConfigurationFromTemplate(): - | AutomaticTransactionGeneratorConfiguration - | undefined { - return this.getTemplateFromFile()?.AutomaticTransactionGenerator; - } }