X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2FChargingStation.ts;h=fc6a409adfc006c7200bc600441f83c8ebeb1490;hb=178956d8c51c6a2b4ecc55b592dfb9ee339b8105;hp=ace7aff1cfb67c29f6c291cdee78bbc7b659a977;hpb=24578c318295d517762fa075959846f230affbe0;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index ace7aff1..fc6a409a 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -26,7 +26,6 @@ import { type OCPPRequestService, OCPPServiceUtils, } from './ocpp'; -import { OCPPConstants } from './ocpp/OCPPConstants'; import { SharedLRUCache } from './SharedLRUCache'; import { BaseError, OCPPError } from '../exception'; import { PerformanceStatistics } from '../performance'; @@ -63,15 +62,17 @@ import { MeterValueMeasurand, type MeterValuesRequest, type MeterValuesResponse, - OCPP16SupportedFeatureProfiles, - OCPP20ConnectorStatusEnumType, OCPPVersion, type OutgoingRequest, PowerUnits, RegistrationStatusEnumType, RequestCommand, + type Reservation, + ReservationFilterKey, + ReservationTerminationReason, type Response, StandardParametersKey, + type Status, type StatusNotificationRequest, type StatusNotificationResponse, StopTransactionReason, @@ -84,7 +85,6 @@ import { WebSocketCloseEventStatusCode, type WsOptions, } from '../types'; -import type { Reservation } from '../types/ocpp/Reservation'; import { ACElectricUtils, AsyncLock, @@ -92,14 +92,16 @@ import { Configuration, Constants, DCElectricUtils, - ErrorUtils, - FileUtils, - MessageChannelUtils, Utils, buildChargingStationAutomaticTransactionGeneratorConfiguration, buildConnectorsStatus, buildEvsesStatus, + buildStartedMessage, + buildStoppedMessage, + buildUpdatedMessage, + handleFileException, logger, + watchJsonFile, } from '../utils'; export class ChargingStation { @@ -136,7 +138,7 @@ export class ChargingStation { private readonly sharedLRUCache: SharedLRUCache; private webSocketPingSetInterval!: NodeJS.Timeout; private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel; - private reservations?: Reservation[]; + private reservationExpirationSetInterval?: NodeJS.Timeout; constructor(index: number, templateFile: string) { this.started = false; @@ -553,7 +555,8 @@ export class ChargingStation { ); } 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` ); } } @@ -581,13 +584,15 @@ 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 ( @@ -595,7 +600,8 @@ export class ChargingStation { Utils.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; } @@ -647,9 +653,12 @@ 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(), @@ -686,7 +695,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...`); @@ -711,7 +720,7 @@ export class ChargingStation { delete this.bootNotificationResponse; this.started = false; this.saveConfiguration(); - parentPort?.postMessage(MessageChannelUtils.buildStoppedMessage(this)); + parentPort?.postMessage(buildStoppedMessage(this)); this.stopping = false; } else { logger.warn(`${this.logPrefix()} Charging station is already stopping...`); @@ -752,11 +761,12 @@ export class ChargingStation { terminateOpened: false, } ): void { - options.handshakeTimeout = options?.handshakeTimeout ?? this.getConnectionTimeout() * 1000; + 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; } @@ -775,7 +785,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; } @@ -823,12 +834,29 @@ export class ChargingStation { public getAutomaticTransactionGeneratorConfiguration(): | AutomaticTransactionGeneratorConfiguration | undefined { + let automaticTransactionGeneratorConfiguration: + | AutomaticTransactionGeneratorConfiguration + | undefined; const automaticTransactionGeneratorConfigurationFromFile = this.getConfigurationFromFile()?.automaticTransactionGenerator; - if (automaticTransactionGeneratorConfigurationFromFile) { - return automaticTransactionGeneratorConfigurationFromFile; + if ( + this.getAutomaticTransactionGeneratorPersistentConfiguration() && + automaticTransactionGeneratorConfigurationFromFile + ) { + automaticTransactionGeneratorConfiguration = + automaticTransactionGeneratorConfigurationFromFile; + } else { + automaticTransactionGeneratorConfiguration = + this.getTemplateFromFile()?.AutomaticTransactionGenerator; } - return this.getTemplateFromFile()?.AutomaticTransactionGenerator; + return { + ...Constants.DEFAULT_ATG_CONFIGURATION, + ...automaticTransactionGeneratorConfiguration, + }; + } + + public getAutomaticTransactionGeneratorStatuses(): Status[] | undefined { + return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses; } public startAutomaticTransactionGenerator(connectorIds?: number[]): void { @@ -840,8 +868,8 @@ export class ChargingStation { } else { this.automaticTransactionGenerator?.start(); } - this.saveChargingStationAutomaticTransactionGeneratorConfiguration(); - parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this)); + this.saveAutomaticTransactionGeneratorConfiguration(); + parentPort?.postMessage(buildUpdatedMessage(this)); } public stopAutomaticTransactionGenerator(connectorIds?: number[]): void { @@ -852,8 +880,8 @@ export class ChargingStation { } else { this.automaticTransactionGenerator?.stop(); } - this.saveChargingStationAutomaticTransactionGeneratorConfiguration(); - parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this)); + this.saveAutomaticTransactionGeneratorConfiguration(); + parentPort?.postMessage(buildUpdatedMessage(this)); } public async stopTransactionOnConnector( @@ -893,78 +921,197 @@ export class ChargingStation { ); } - public supportsReservations(): boolean { - logger.info(`${this.logPrefix()} Check for reservation support in charging station`); - return ChargingStationConfigurationUtils.getConfigurationKey( - this, - StandardParametersKey.SupportedFeatureProfiles - ).value.includes(OCPP16SupportedFeatureProfiles.Reservation); - } - - public supportsReservationsOnConnectorId0(): boolean { - logger.info( - `Check for reservation support on connector 0 in charging station (CS): ${this.logPrefix()}` - ); - return ( - this.supportsReservations() && + public getReservationOnConnectorId0Enabled(): boolean { + return Utils.convertToBoolean( ChargingStationConfigurationUtils.getConfigurationKey( this, - OCPPConstants.OCPP_RESERVE_CONNECTOR_ZERO_SUPPORTED - ).value === 'true' + StandardParametersKey.ReserveConnectorZeroSupported + ).value ); } - public addReservation(newReservation: Reservation): void { - if (Utils.isNullOrUndefined(this.reservations)) { - this.reservations = []; - } - const [exists, foundReservation] = this.doesReservationExist(newReservation.reservationId); + public async addReservation(reservation: Reservation): Promise { + const [exists, reservationFound] = this.doesReservationExists(reservation); if (exists) { - this.replaceExistingReservation(foundReservation, newReservation); - } else { - this.reservations.push(newReservation); + await this.removeReservation(reservationFound); + } + const connectorStatus = this.getConnectorStatus(reservation.connectorId); + connectorStatus.reservation = reservation; + connectorStatus.status = ConnectorStatusEnum.Reserved; + if (reservation.connectorId === 0) { + return; } + await this.ocppRequestService.requestHandler< + StatusNotificationRequest, + StatusNotificationResponse + >( + this, + RequestCommand.STATUS_NOTIFICATION, + OCPPServiceUtils.buildStatusNotificationRequest( + this, + reservation.connectorId, + ConnectorStatusEnum.Reserved + ) + ); } - public removeReservation(existingReservationId: number): void { - const index = this.reservations.findIndex((res) => res.reservationId === existingReservationId); - this.reservations.splice(index, 1); + public async removeReservation( + reservation: Reservation, + reason?: ReservationTerminationReason + ): Promise { + const connector = this.getConnectorStatus(reservation.connectorId); + switch (reason) { + case ReservationTerminationReason.TRANSACTION_STARTED: { + delete connector.reservation; + if (reservation.connectorId === 0) { + connector.status = ConnectorStatusEnum.Available; + } + break; + } + case ReservationTerminationReason.CONNECTOR_STATE_CHANGED: { + delete connector.reservation; + break; + } + default: { + // ReservationTerminationReason.EXPIRED, ReservationTerminationReason.CANCELED + connector.status = ConnectorStatusEnum.Available; + delete connector.reservation; + await this.ocppRequestService.requestHandler< + StatusNotificationRequest, + StatusNotificationResponse + >( + this, + RequestCommand.STATUS_NOTIFICATION, + OCPPServiceUtils.buildStatusNotificationRequest( + this, + reservation.connectorId, + ConnectorStatusEnum.Available + ) + ); + break; + } + } } - public getReservation(reservationId: number, reservationIndex?: number): Reservation { - if (!Utils.isNullOrUndefined(reservationIndex)) { - return this.reservations[reservationIndex]; + public getReservationBy(filterKey: ReservationFilterKey, value: number | string): Reservation { + if (this.hasEvses) { + for (const evse of this.evses.values()) { + for (const connector of evse.connectors.values()) { + if (connector?.reservation?.[filterKey] === value) { + return connector.reservation; + } + } + } + } else { + for (const connector of this.connectors.values()) { + if (connector?.reservation?.[filterKey] === value) { + return connector.reservation; + } + } } - return this.reservations.find((r) => r.reservationId === reservationId); } - public doesReservationExist( - reservationId: number, - reservation?: Reservation - ): [boolean, Reservation] { - const foundReservation = this.reservations.find( - (r) => r.reservationId === reservationId || r.reservationId === reservation.reservationId + public doesReservationExists(reservation: Partial): [boolean, Reservation] { + const foundReservation = this.getReservationBy( + ReservationFilterKey.RESERVATION_ID, + reservation?.id ); return Utils.isUndefined(foundReservation) ? [false, null] : [true, foundReservation]; } - public getReservationByConnectorId(connectorId: number): Reservation { - return this.reservations.find((r) => r.connectorId === connectorId); + 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 => { + if (this.hasEvses) { + for (const evse of this.evses.values()) { + for (const connector of evse.connectors.values()) { + if (connector?.reservation?.expiryDate.toString() < new Date().toISOString()) { + await this.removeReservation(connector.reservation); + } + } + } + } else { + for (const connector of this.connectors.values()) { + if (connector?.reservation?.expiryDate.toString() < new Date().toISOString()) { + await this.removeReservation(connector.reservation); + } + } + } + }, interval); + } + + public restartReservationExpiryDateSetInterval(): void { + this.stopReservationExpirationSetInterval(); + this.startReservationExpirationSetInterval(); + } + + public validateIncomingRequestWithReservation(connectorId: number, idTag: string): boolean { + const reservation = this.getReservationBy(ReservationFilterKey.CONNECTOR_ID, connectorId); + return !Utils.isUndefined(reservation) && reservation.idTag === idTag; } - public getAvailableConnector(): Map { - for (const connectorId in this.connectors) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const connector = this.connectors[Utils.convertToInt(connectorId)]; - if ( - this.isConnectorAvailable(Utils.convertToInt(connectorId)) && - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - connector.status === OCPP20ConnectorStatusEnumType.Available - ) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return connector; + public isConnectorReservable( + reservationId: number, + idTag?: string, + connectorId?: number + ): boolean { + const [alreadyExists] = this.doesReservationExists({ id: reservationId }); + if (alreadyExists) { + return alreadyExists; + } + const userReservedAlready = Utils.isUndefined( + this.getReservationBy(ReservationFilterKey.ID_TAG, idTag) + ) + ? false + : true; + const notConnectorZero = Utils.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 evse of this.evses.values()) { + reservableConnectors = this.countReservableConnectors(evse.connectors); + } + } else { + reservableConnectors = this.countReservableConnectors(this.connectors); + } + return reservableConnectors - this.getNumberOfReservationsOnConnectorZero(); + } + + private countReservableConnectors(connectors: Map) { + let reservableConnectors = 0; + for (const [connectorId, connector] of connectors) { + if (connectorId === 0) { + continue; + } + if (connector.status === ConnectorStatusEnum.Available) { + ++reservableConnectors; } } + return reservableConnectors; + } + + private getNumberOfReservationsOnConnectorZero(): number { + let numberOfReservations = 0; + if (this.hasEvses) { + for (const evse of this.evses.values()) { + if (evse.connectors.get(0)?.reservation) { + ++numberOfReservations; + } + } + } else if (this.connectors.get(0)?.reservation) { + ++numberOfReservations; + } + return numberOfReservations; } private flushMessageBuffer(): void { @@ -994,14 +1141,10 @@ export class ChargingStation { return this.stationInfo.supervisionUrlOcppConfiguration ?? false; } - private replaceExistingReservation( - existingReservation: Reservation, - newReservation: Reservation - ): void { - const existingReservationIndex = this.reservations.findIndex( - (r) => r.reservationId === existingReservation.reservationId - ); - this.reservations.splice(existingReservationIndex, 1, newReservation); + private stopReservationExpirationSetInterval(): void { + if (this.reservationExpirationSetInterval) { + clearInterval(this.reservationExpirationSetInterval); + } } private getSupervisionUrlOcppKey(): string { @@ -1028,7 +1171,7 @@ export class ChargingStation { this.templateFileHash = template.templateHash; } } catch (error) { - ErrorUtils.handleFileException( + handleFileException( this.templateFile, FileType.ChargingStationTemplate, error as NodeJS.ErrnoException, @@ -1147,8 +1290,13 @@ export class ChargingStation { return this.stationInfo?.stationInfoPersistentConfiguration ?? true; } + private getAutomaticTransactionGeneratorPersistentConfiguration(): boolean { + return this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration ?? true; + } + private handleUnsupportedVersion(version: OCPPVersion) { - const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`; + const errorMsg = `Unsupported protocol version '${version}' configured + in template file ${this.templateFile}`; logger.error(`${this.logPrefix()} ${errorMsg}`); throw new BaseError(errorMsg); } @@ -1161,9 +1309,8 @@ export class ChargingStation { `${ChargingStationUtils.getHashId(this.index, stationTemplate)}.json` ); const chargingStationConfiguration = this.getConfigurationFromFile(); - const featureFlag = false; if ( - featureFlag && + chargingStationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash && (chargingStationConfiguration?.connectorsStatus || chargingStationConfiguration?.evsesStatus) ) { this.initializeConnectorsOrEvsesFromFile(chargingStationConfiguration); @@ -1546,7 +1693,7 @@ export class ChargingStation { if (stationTemplate?.Evses) { const evsesConfigHash = crypto .createHash(Constants.DEFAULT_HASH_ALGORITHM) - .update(`${JSON.stringify(stationTemplate?.Evses)}`) + .update(JSON.stringify(stationTemplate?.Evses)) .digest('hex'); const evsesConfigChanged = this.evses?.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash; @@ -1607,7 +1754,7 @@ export class ChargingStation { this.configurationFileHash = configuration.configurationHash; } } catch (error) { - ErrorUtils.handleFileException( + handleFileException( this.configurationFile, FileType.ChargingStationConfiguration, error as NodeJS.ErrnoException, @@ -1618,8 +1765,10 @@ export class ChargingStation { return configuration; } - private saveChargingStationAutomaticTransactionGeneratorConfiguration(): void { - this.saveConfiguration(); + private saveAutomaticTransactionGeneratorConfiguration(): void { + if (this.getAutomaticTransactionGeneratorPersistentConfiguration()) { + this.saveConfiguration(); + } } private saveConnectorsStatus() { @@ -1640,24 +1789,44 @@ export class ChargingStation { Utils.cloneObject(this.getConfigurationFromFile()) ?? {}; if (this.getStationInfoPersistentConfiguration() && this.stationInfo) { configurationData.stationInfo = this.stationInfo; + } else { + delete configurationData.stationInfo; } if (this.getOcppPersistentConfiguration() && this.ocppConfiguration?.configurationKey) { configurationData.configurationKey = this.ocppConfiguration.configurationKey; + } else { + delete configurationData.configurationKey; } configurationData = merge( configurationData, buildChargingStationAutomaticTransactionGeneratorConfiguration(this) ); + if ( + !this.getAutomaticTransactionGeneratorPersistentConfiguration() || + !this.getAutomaticTransactionGeneratorConfiguration() + ) { + delete configurationData.automaticTransactionGenerator; + } 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)) + .update( + JSON.stringify({ + stationInfo: configurationData.stationInfo, + configurationKey: configurationData.configurationKey, + automaticTransactionGenerator: configurationData.automaticTransactionGenerator, + } as ChargingStationConfiguration) + ) .digest('hex'); if (this.configurationFileHash !== configurationHash) { AsyncLock.acquire(AsyncLockType.configuration) @@ -1674,7 +1843,7 @@ export class ChargingStation { this.configurationFileHash = configurationHash; }) .catch((error) => { - ErrorUtils.handleFileException( + handleFileException( this.configurationFile, FileType.ChargingStationConfiguration, error as NodeJS.ErrnoException, @@ -1692,7 +1861,7 @@ export class ChargingStation { ); } } catch (error) { - ErrorUtils.handleFileException( + handleFileException( this.configurationFile, FileType.ChargingStationConfiguration, error as NodeJS.ErrnoException, @@ -1743,7 +1912,7 @@ export class ChargingStation { skipBufferingOnError: true, }); if (this.isRegistered() === false) { - this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++; + this.getRegistrationMaxRetries() !== -1 && ++registrationRetryCount; await Utils.sleep( this?.bootNotificationResponse?.interval ? this.bootNotificationResponse.interval * 1000 @@ -1767,7 +1936,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` @@ -1797,7 +1966,7 @@ export class ChargingStation { this.started === true && (await this.reconnect()); break; } - parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this)); + parentPort?.postMessage(buildUpdatedMessage(this)); } private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined { @@ -1907,7 +2076,7 @@ export class ChargingStation { 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, @@ -2305,7 +2474,7 @@ export class ChargingStation { this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() || this.getAutoReconnectMaxRetries() === -1 ) { - this.autoReconnectRetryCount++; + ++this.autoReconnectRetryCount; const reconnectDelay = this.getReconnectExponentialDelay() ? Utils.exponentialDelay(this.autoReconnectRetryCount) : this.getConnectionTimeout() * 1000;