X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2FChargingStation.ts;h=bd3fb81283b079f66ceed55b380434666c331613;hb=b3b3f0eb5e187646c6502a9d36939e05c80b5939;hp=d08079c805760799b7e47d0f9c82dafc8bd7935a;hpb=cfc9875aed1b5e773f4baaa30574f1a06534f566;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index d08079c8..bd3fb812 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -1,15 +1,7 @@ // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved. import { createHash } from 'node:crypto'; -import { - type FSWatcher, - closeSync, - existsSync, - mkdirSync, - openSync, - readFileSync, - writeFileSync, -} from 'node:fs'; +import { type FSWatcher, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { URL } from 'node:url'; import { parentPort } from 'node:worker_threads'; @@ -28,6 +20,7 @@ import { } from './ConfigurationKeyUtils'; import { buildConnectorsMap, + checkChargingStation, checkConnectorsConfiguration, checkStationInfoConnectorStatus, checkTemplate, @@ -151,10 +144,12 @@ import { isUndefined, logPrefix, logger, + min, + once, roundTo, secureRandom, sleep, - watchJsonFile, + // watchJsonFile, } from '../utils'; export class ChargingStation { @@ -166,7 +161,7 @@ export class ChargingStation { public idTagsCache: IdTagsCache; public automaticTransactionGenerator!: AutomaticTransactionGenerator | undefined; public ocppConfiguration!: ChargingStationOcppConfiguration | undefined; - public wsConnection!: WebSocket | null; + public wsConnection: WebSocket | null; public readonly connectors: Map; public readonly evses: Map; public readonly requests: Map; @@ -198,6 +193,7 @@ export class ChargingStation { this.started = false; this.starting = false; this.stopping = false; + this.wsConnection = null; this.wsConnectionRestarted = false; this.autoReconnectRetryCount = 0; this.index = index; @@ -363,12 +359,7 @@ export class ChargingStation { } public getMaximumPower(stationInfo?: ChargingStationInfo): number { - const localStationInfo = stationInfo ?? this.stationInfo; - // eslint-disable-next-line @typescript-eslint/dot-notation - return ( - (localStationInfo['maxPower' as keyof ChargingStationInfo] as number) ?? - localStationInfo.maximumPower - ); + return (stationInfo ?? this.stationInfo).maximumPower!; } public getConnectorMaximumAvailablePower(connectorId: number): number { @@ -391,7 +382,7 @@ export class ChargingStation { const connectorMaximumPower = this.getMaximumPower() / this.powerDivider; const connectorChargingProfilesPowerLimit = getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId); - return Math.min( + return min( isNaN(connectorMaximumPower) ? Infinity : connectorMaximumPower, isNaN(connectorAmperageLimitationPowerLimit!) ? Infinity @@ -573,8 +564,7 @@ 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`, ); } } @@ -609,8 +599,7 @@ export class ChargingStation { } 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 ( @@ -618,8 +607,7 @@ export class ChargingStation { 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; } @@ -676,44 +664,55 @@ export class ChargingStation { } this.openWSConnection(); // Monitor charging station template file - this.templateFileWatcher = watchJsonFile( - this.templateFile, - FileType.ChargingStationTemplate, - this.logPrefix(), - undefined, - (event, filename): void => { - if (isNotEmptyString(filename) && event === 'change') { - try { - logger.debug( - `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${ - this.templateFile - } file have changed, reload`, - ); - this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash); - // Initialize - this.initialize(); - this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo)!); - // Restart the ATG - this.stopAutomaticTransactionGenerator(); - delete this.automaticTransactionGeneratorConfiguration; - if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) { - this.startAutomaticTransactionGenerator(); - } - if (this.getEnableStatistics() === true) { - this.performanceStatistics?.restart(); - } else { - this.performanceStatistics?.stop(); - } - // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed - } catch (error) { - logger.error( - `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`, - error, - ); - } - } - }, - ); + // FIXME: Disabled until the spurious configuration file change detection is identified + // this.templateFileWatcher = watchJsonFile( + // this.templateFile, + // FileType.ChargingStationTemplate, + // this.logPrefix(), + // undefined, + // (event, filename): void => { + // if (isNotEmptyString(filename) && event === 'change') { + // try { + // logger.debug( + // `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${ + // this.templateFile + // } file have changed, reload`, + // ); + // this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash); + // // Initialize + // this.initialize(); + // this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo)!); + // // Restart the ATG + // this.stopAutomaticTransactionGenerator() + // .then(() => { + // delete this.automaticTransactionGeneratorConfiguration; + // if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) { + // this.startAutomaticTransactionGenerator(); + // } + // }) + // .catch((err) => + // logger.error( + // `${this.logPrefix()} failed to stop ATG at ${ + // FileType.ChargingStationTemplate + // } reload`, + // err, + // ), + // ); + // if (this.getEnableStatistics() === true) { + // this.performanceStatistics?.restart(); + // } else { + // this.performanceStatistics?.stop(); + // } + // // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed + // } catch (error) { + // logger.error( + // `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`, + // error, + // ); + // } + // } + // }, + // ); this.started = true; parentPort?.postMessage(buildStartedMessage(this)); this.starting = false; @@ -771,19 +770,16 @@ export class ChargingStation { } public openWSConnection( - options: WsOptions = this.stationInfo?.wsOptions ?? {}, - params: { closeOpened?: boolean; terminateOpened?: boolean } = { - closeOpened: false, - terminateOpened: false, - }, + options?: WsOptions, + params?: { closeOpened?: boolean; terminateOpened?: boolean }, ): void { - options = { handshakeTimeout: secondsToMilliseconds(this.getConnectionTimeout()), ...options }; + options = { + handshakeTimeout: secondsToMilliseconds(this.getConnectionTimeout()), + ...this.stationInfo?.wsOptions, + ...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`, - ); + if (!checkChargingStation(this, this.logPrefix())) { return; } if ( @@ -801,8 +797,7 @@ 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; } @@ -889,13 +884,13 @@ export class ChargingStation { parentPort?.postMessage(buildUpdatedMessage(this)); } - public stopAutomaticTransactionGenerator(connectorIds?: number[]): void { + public async stopAutomaticTransactionGenerator(connectorIds?: number[]): Promise { if (isNotEmptyArray(connectorIds)) { for (const connectorId of connectorIds!) { - this.automaticTransactionGenerator?.stopConnector(connectorId); + await this.automaticTransactionGenerator?.stopConnector(connectorId); } } else { - this.automaticTransactionGenerator?.stop(); + await this.automaticTransactionGenerator?.stop(); } this.saveAutomaticTransactionGeneratorConfiguration(); parentPort?.postMessage(buildUpdatedMessage(this)); @@ -1036,8 +1031,7 @@ export class ChargingStation { } private startReservationExpirationSetInterval(customInterval?: number): void { - const interval = - customInterval ?? Constants.DEFAULT_RESERVATION_EXPIRATION_OBSERVATION_INTERVAL; + const interval = customInterval ?? Constants.DEFAULT_RESERVATION_EXPIRATION_INTERVAL; if (interval > 0) { logger.info( `${this.logPrefix()} Reservation expiration date checks started every ${formatDurationMilliSeconds( @@ -1051,7 +1045,7 @@ export class ChargingStation { } private stopReservationExpirationSetInterval(): void { - if (this.reservationExpirationSetInterval) { + if (!isNullOrUndefined(this.reservationExpirationSetInterval)) { clearInterval(this.reservationExpirationSetInterval); } } @@ -1145,7 +1139,8 @@ export class ChargingStation { private getStationInfoFromTemplate(): ChargingStationInfo { const stationTemplate: ChargingStationTemplate = this.getTemplateFromFile()!; checkTemplate(stationTemplate, this.logPrefix(), this.templateFile); - warnTemplateKeysDeprecation(stationTemplate, this.logPrefix(), this.templateFile); + const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation, this); + warnTemplateKeysDeprecationOnce(stationTemplate, this.logPrefix(), this.templateFile); if (stationTemplate?.Connectors) { checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile); } @@ -1244,8 +1239,7 @@ 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); } @@ -1512,7 +1506,7 @@ export class ChargingStation { for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) { if ( connectorId === 0 && - (!stationTemplate?.Connectors[connectorId] || + (!stationTemplate?.Connectors?.[connectorId] || this.getUseConnectorId0(stationTemplate) === false) ) { continue; @@ -1569,6 +1563,13 @@ export class ChargingStation { } with evse id 0 with no connector id 0 configuration`, ); } + if (Object.keys(stationTemplate?.Evses?.[0]?.Connectors as object).length > 1) { + logger.warn( + `${this.logPrefix()} Charging station information from template ${ + this.templateFile + } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`, + ); + } if (stationTemplate?.Evses) { const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM) .update(JSON.stringify(stationTemplate?.Evses)) @@ -1580,11 +1581,11 @@ export class ChargingStation { this.evsesConfigurationHash = evsesConfigHash; const templateMaxEvses = getMaxNumberOfEvses(stationTemplate?.Evses); if (templateMaxEvses > 0) { - for (const evse in stationTemplate.Evses) { - const evseId = convertToInt(evse); + for (const evseKey in stationTemplate.Evses) { + const evseId = convertToInt(evseKey); this.evses.set(evseId, { connectors: buildConnectorsMap( - stationTemplate?.Evses[evse]?.Connectors, + stationTemplate?.Evses[evseKey]?.Connectors, this.logPrefix(), this.templateFile, ), @@ -1704,30 +1705,27 @@ export class ChargingStation { ) .digest('hex'); if (this.configurationFileHash !== configurationHash) { - 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); - }); + AsyncLock.runExclusive(AsyncLockType.configuration, () => { + configurationData.configurationHash = configurationHash; + const measureId = `${FileType.ChargingStationConfiguration} write`; + const beginId = PerformanceStatistics.beginMeasure(measureId); + writeFileSync( + this.configurationFile, + JSON.stringify(configurationData, undefined, 2), + 'utf8', + ); + 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(), + ); + }); } else { logger.debug( `${this.logPrefix()} Not saving unchanged charging station configuration file ${ @@ -2075,12 +2073,10 @@ export class ChargingStation { // -1 for unlimited, 0 for disabling private getAutoReconnectMaxRetries(): number | undefined { - return ( - this.stationInfo.autoReconnectMaxRetries ?? Configuration.getAutoReconnectMaxRetries() ?? -1 - ); + return this.stationInfo.autoReconnectMaxRetries ?? -1; } - // 0 for disabling + // -1 for unlimited, 0 for disabling private getRegistrationMaxRetries(): number | undefined { return this.stationInfo.registrationMaxRetries ?? -1; } @@ -2186,7 +2182,7 @@ export class ChargingStation { this.stopHeartbeat(); // Stop ongoing transactions if (this.automaticTransactionGenerator?.started === true) { - this.stopAutomaticTransactionGenerator(); + await this.stopAutomaticTransactionGenerator(); } else { await this.stopRunningTransactions(reason); } @@ -2333,7 +2329,7 @@ export class ChargingStation { this.stopHeartbeat(); // Stop the ATG if needed if (this.getAutomaticTransactionGeneratorConfiguration().stopOnConnectionFailure === true) { - this.stopAutomaticTransactionGenerator(); + await this.stopAutomaticTransactionGenerator(); } if ( this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries()! || @@ -2360,7 +2356,6 @@ export class ChargingStation { ); this.openWSConnection( { - ...(this.stationInfo?.wsOptions ?? {}), handshakeTimeout: reconnectTimeout, }, { closeOpened: true },