X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2FChargingStation.ts;h=a3f6769607c5b01bb9774bf614a71f31a80a5afe;hb=d58b442097da31f8b974d51aef63c64470d9ab48;hp=414a48c520e08cae18bcffba836d20b504a62516;hpb=5ced7e80e407f1ea468d4adf2fb40341dde978de;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 414a48c5..a3f67696 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -67,6 +67,9 @@ import { PowerUnits, RegistrationStatusEnumType, RequestCommand, + type Reservation, + ReservationFilterKey, + ReservationTerminationReason, type Response, StandardParametersKey, type Status, @@ -89,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 { @@ -133,6 +138,7 @@ export class ChargingStation { private readonly sharedLRUCache: SharedLRUCache; private webSocketPingSetInterval!: NodeJS.Timeout; private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel; + private reservationExpiryDateSetInterval?: NodeJS.Timeout; constructor(index: number, templateFile: string) { this.started = false; @@ -549,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` ); } } @@ -577,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 ( @@ -591,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; } @@ -643,9 +653,12 @@ export class ChargingStation { if (this.getEnableStatistics() === true) { this.performanceStatistics?.start(); } + if (this.hasFeatureProfile(SupportedFeatureProfiles.Reservation)) { + this.startReservationExpiryDateSetInterval(); + } this.openWSConnection(); // Monitor charging station template file - this.templateFileWatcher = FileUtils.watchJsonFile( + this.templateFileWatcher = watchJsonFile( this.templateFile, FileType.ChargingStationTemplate, this.logPrefix(), @@ -682,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...`); @@ -707,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...`); @@ -748,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; } @@ -771,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; } @@ -853,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 { @@ -865,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( @@ -906,6 +921,199 @@ export class ChargingStation { ); } + public getReservationOnConnectorId0Enabled(): boolean { + return Utils.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); + } + 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 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 getReservationBy(key: string, value: number | string): Reservation { + if (this.hasEvses) { + for (const evse of this.evses.values()) { + for (const connector of evse.connectors.values()) { + if (connector?.reservation?.[key] === value) { + return connector.reservation; + } + } + } + } else { + for (const connector of this.connectors.values()) { + if (connector?.reservation?.[key] === value) { + return connector.reservation; + } + } + } + } + + public doesReservationExists(reservation: Partial): [boolean, Reservation] { + const foundReservation = this.getReservationBy( + ReservationFilterKey.RESERVATION_ID, + reservation?.id + ); + return Utils.isUndefined(foundReservation) ? [false, null] : [true, foundReservation]; + } + + public startReservationExpiryDateSetInterval(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 CS now` + ); + // eslint-disable-next-line @typescript-eslint/no-misused-promises + this.reservationExpiryDateSetInterval = 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.stopReservationExpiryDateSetInterval(); + this.startReservationExpiryDateSetInterval(); + } + + public validateIncomingRequestWithReservation(connectorId: number, idTag: string): boolean { + const reservation = this.getReservationBy(ReservationFilterKey.CONNECTOR_ID, connectorId); + return !Utils.isUndefined(reservation) && reservation.idTag === idTag; + } + + 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 [id, connector] of connectors) { + if (id === 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 { if (this.messageBuffer.size > 0) { for (const message of this.messageBuffer.values()) { @@ -933,6 +1141,12 @@ export class ChargingStation { return this.stationInfo.supervisionUrlOcppConfiguration ?? false; } + private stopReservationExpiryDateSetInterval(): void { + if (this.reservationExpiryDateSetInterval) { + clearInterval(this.reservationExpiryDateSetInterval); + } + } + private getSupervisionUrlOcppKey(): string { return this.stationInfo.supervisionUrlOcppKey ?? VendorParametersKey.ConnectionUrl; } @@ -957,7 +1171,7 @@ export class ChargingStation { this.templateFileHash = template.templateHash; } } catch (error) { - ErrorUtils.handleFileException( + handleFileException( this.templateFile, FileType.ChargingStationTemplate, error as NodeJS.ErrnoException, @@ -1081,7 +1295,8 @@ export class ChargingStation { } 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); } @@ -1094,10 +1309,9 @@ export class ChargingStation { `${ChargingStationUtils.getHashId(this.index, stationTemplate)}.json` ); const chargingStationConfiguration = this.getConfigurationFromFile(); - // FIXME: template changes to evses or connectors are not taken into account if ( - chargingStationConfiguration?.connectorsStatus || - chargingStationConfiguration?.evsesStatus + chargingStationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash && + (chargingStationConfiguration?.connectorsStatus || chargingStationConfiguration?.evsesStatus) ) { this.initializeConnectorsOrEvsesFromFile(chargingStationConfiguration); } else { @@ -1479,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; @@ -1540,7 +1754,7 @@ export class ChargingStation { this.configurationFileHash = configuration.configurationHash; } } catch (error) { - ErrorUtils.handleFileException( + handleFileException( this.configurationFile, FileType.ChargingStationConfiguration, error as NodeJS.ErrnoException, @@ -1551,7 +1765,7 @@ export class ChargingStation { return configuration; } - private saveChargingStationAutomaticTransactionGeneratorConfiguration(): void { + private saveAutomaticTransactionGeneratorConfiguration(): void { if (this.getAutomaticTransactionGeneratorPersistentConfiguration()) { this.saveConfiguration(); } @@ -1629,7 +1843,7 @@ export class ChargingStation { this.configurationFileHash = configurationHash; }) .catch((error) => { - ErrorUtils.handleFileException( + handleFileException( this.configurationFile, FileType.ChargingStationConfiguration, error as NodeJS.ErrnoException, @@ -1647,7 +1861,7 @@ export class ChargingStation { ); } } catch (error) { - ErrorUtils.handleFileException( + handleFileException( this.configurationFile, FileType.ChargingStationConfiguration, error as NodeJS.ErrnoException, @@ -1722,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` @@ -1752,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 { @@ -1862,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,