X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2FChargingStation.ts;h=82fba5adad98c6629a6579b8e7c8516df38bb7bb;hb=d990f4bc8ad5366a200fd3d89be76c084fd71cea;hp=c8eaf5e44bc70e18844a37ac79390db7a8b845bc;hpb=244c1396e337032577839fa13e9191d5e943864f;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index c8eaf5e4..82fba5ad 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -2,13 +2,13 @@ import { createHash } from 'node:crypto' import { EventEmitter } from 'node:events' -import { type FSWatcher, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { type FSWatcher, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { dirname, join, parse } from 'node:path' import { URL } from 'node:url' import { parentPort } from 'node:worker_threads' import { millisecondsToSeconds, secondsToMilliseconds } from 'date-fns' -import merge from 'just-merge' +import { mergeDeepRight } from 'rambda' import { type RawData, WebSocket } from 'ws' import { AutomaticTransactionGenerator } from './AutomaticTransactionGenerator.js' @@ -42,6 +42,7 @@ import { hasReservationExpired, initializeConnectorsMapStatus, propagateSerialNumber, + setChargingStationOptions, stationTemplateToStationInfo, warnTemplateKeysDeprecation } from './Helpers.js' @@ -73,6 +74,7 @@ import { ChargingStationEvents, type ChargingStationInfo, type ChargingStationOcppConfiguration, + type ChargingStationOptions, type ChargingStationTemplate, type ConnectorStatus, ConnectorStatusEnum, @@ -86,7 +88,6 @@ import { FirmwareStatus, type FirmwareStatusNotificationRequest, type FirmwareStatusNotificationResponse, - type FirmwareUpgrade, type HeartbeatRequest, type HeartbeatResponse, type IncomingRequest, @@ -126,6 +127,7 @@ import { buildAddedMessage, buildChargingStationAutomaticTransactionGeneratorConfiguration, buildConnectorsStatus, + buildDeletedMessage, buildEvsesStatus, buildStartedMessage, buildStoppedMessage, @@ -159,13 +161,13 @@ export class ChargingStation extends EventEmitter { public started: boolean public starting: boolean public idTagsCache: IdTagsCache - public automaticTransactionGenerator!: AutomaticTransactionGenerator | undefined - public ocppConfiguration!: ChargingStationOcppConfiguration | undefined + public automaticTransactionGenerator?: AutomaticTransactionGenerator + public ocppConfiguration?: ChargingStationOcppConfiguration public wsConnection: WebSocket | null public readonly connectors: Map public readonly evses: Map public readonly requests: Map - public performanceStatistics!: PerformanceStatistics | undefined + public performanceStatistics?: PerformanceStatistics public heartbeatSetInterval?: NodeJS.Timeout public ocppRequestService!: OCPPRequestService public bootNotificationRequest?: BootNotificationRequest @@ -182,14 +184,14 @@ export class ChargingStation extends EventEmitter { private configuredSupervisionUrl!: URL private wsConnectionRetried: boolean private wsConnectionRetryCount: number - private templateFileWatcher!: FSWatcher | undefined + private templateFileWatcher?: FSWatcher private templateFileHash!: string private readonly sharedLRUCache: SharedLRUCache private wsPingSetInterval?: NodeJS.Timeout private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel private flushMessageBufferSetInterval?: NodeJS.Timeout - constructor (index: number, templateFile: string) { + constructor (index: number, templateFile: string, options?: ChargingStationOptions) { super() this.started = false this.starting = false @@ -210,6 +212,9 @@ export class ChargingStation extends EventEmitter { this.on(ChargingStationEvents.added, () => { parentPort?.postMessage(buildAddedMessage(this)) }) + this.on(ChargingStationEvents.deleted, () => { + parentPort?.postMessage(buildDeletedMessage(this)) + }) this.on(ChargingStationEvents.started, () => { parentPort?.postMessage(buildStartedMessage(this)) }) @@ -243,16 +248,20 @@ export class ChargingStation extends EventEmitter { } }) - this.initialize() + this.initialize(options) + + this.add() - this.stationInfo?.autoStart === true && this.start() + if (this.stationInfo?.autoStart === true) { + this.start() + } } public get hasEvses (): boolean { return this.connectors.size === 0 && this.evses.size > 0 } - private get wsConnectionUrl (): URL { + public get wsConnectionUrl (): URL { return new URL( `${ this.stationInfo?.supervisionUrlOcppConfiguration === true && @@ -534,8 +543,8 @@ export class ChargingStation extends EventEmitter { } else { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.stationInfo!.supervisionUrls = url - this.saveStationInfo() this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl() + this.saveStationInfo() } } @@ -652,10 +661,28 @@ export class ChargingStation extends EventEmitter { } } - public add (): void { + private add (): void { this.emit(ChargingStationEvents.added) } + public async delete (deleteConfiguration = true): Promise { + if (this.started) { + await this.stop() + } + AutomaticTransactionGenerator.deleteInstance(this) + PerformanceStatistics.deleteInstance(this.stationInfo?.hashId) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!) + this.requests.clear() + this.connectors.clear() + this.evses.clear() + this.templateFileWatcher?.unref() + deleteConfiguration && rmSync(this.configurationFile, { force: true }) + this.chargingStationWorkerBroadcastChannel.unref() + this.emit(ChargingStationEvents.deleted) + this.removeAllListeners() + } + public start (): void { if (!this.started) { if (!this.starting) { @@ -679,10 +706,10 @@ export class ChargingStation extends EventEmitter { } file have changed, reload` ) this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash) - // Initialize - this.initialize() // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!) + // Initialize + this.initialize() // Restart the ATG const ATGStarted = this.automaticTransactionGenerator?.started if (ATGStarted === true) { @@ -733,12 +760,11 @@ export class ChargingStation extends EventEmitter { if (this.stationInfo?.enableStatistics === true) { this.performanceStatistics?.stop() } - this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash) this.templateFileWatcher?.close() - this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash) delete this.bootNotificationResponse this.started = false this.saveConfiguration() + this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash) this.emit(ChargingStationEvents.stopped) this.stopping = false } else { @@ -793,14 +819,12 @@ export class ChargingStation extends EventEmitter { if (this.isWebSocketConnectionOpened()) { logger.warn( - `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened` + `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.href} is already opened` ) return } - logger.info( - `${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.toString()}` - ) + logger.info(`${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.href}`) this.wsConnection = new WebSocket( this.wsConnectionUrl, @@ -1008,15 +1032,14 @@ export class ChargingStation extends EventEmitter { connectorId?: number ): boolean { const reservation = this.getReservationBy('reservationId', reservationId) - const reservationExists = reservation !== undefined && !hasReservationExpired(reservation) + const reservationExists = reservation != null && !hasReservationExpired(reservation) if (arguments.length === 1) { return !reservationExists } else if (arguments.length > 1) { - const userReservation = - idTag !== undefined ? this.getReservationBy('idTag', idTag) : undefined + const userReservation = idTag != null ? this.getReservationBy('idTag', idTag) : undefined const userReservationExists = - userReservation !== undefined && !hasReservationExpired(userReservation) - const notConnectorZero = connectorId === undefined ? true : connectorId > 0 + userReservation != null && !hasReservationExpired(userReservation) + const notConnectorZero = connectorId == null ? true : connectorId > 0 const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0 return ( !reservationExists && !userReservationExists && notConnectorZero && freeConnectorsAvailable @@ -1139,10 +1162,9 @@ export class ChargingStation extends EventEmitter { } const stationInfo = stationTemplateToStationInfo(stationTemplate) stationInfo.hashId = getHashId(this.index, stationTemplate) - stationInfo.autoStart = stationTemplate.autoStart ?? true + stationInfo.templateIndex = this.index stationInfo.templateName = parse(this.templateFile).name stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate) - stationInfo.ocppVersion = stationTemplate.ocppVersion ?? OCPPVersion.VERSION_16 createSerialNumber(stationTemplate, stationInfo) stationInfo.voltageOut = this.getVoltageOut(stationInfo) if (isNotEmptyArray(stationTemplate.power)) { @@ -1159,9 +1181,8 @@ export class ChargingStation extends EventEmitter { : stationTemplate.power } stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo) - stationInfo.firmwareVersionPattern = - stationTemplate.firmwareVersionPattern ?? Constants.SEMVER_PATTERN if ( + isNotEmptyString(stationInfo.firmwareVersionPattern) && isNotEmptyString(stationInfo.firmwareVersion) && !new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion) ) { @@ -1171,7 +1192,7 @@ export class ChargingStation extends EventEmitter { } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'` ) } - stationInfo.firmwareUpgrade = merge( + stationInfo.firmwareUpgrade = mergeDeepRight( { versionUpgrade: { step: 1 @@ -1180,36 +1201,39 @@ export class ChargingStation extends EventEmitter { }, stationTemplate.firmwareUpgrade ?? {} ) - stationInfo.resetTime = - stationTemplate.resetTime != null - ? secondsToMilliseconds(stationTemplate.resetTime) - : Constants.DEFAULT_CHARGING_STATION_RESET_TIME + if (stationTemplate.resetTime != null) { + stationInfo.resetTime = secondsToMilliseconds(stationTemplate.resetTime) + } return stationInfo } private getStationInfoFromFile ( - stationInfoPersistentConfiguration = true + stationInfoPersistentConfiguration: boolean | undefined = Constants.DEFAULT_STATION_INFO + .stationInfoPersistentConfiguration ): ChargingStationInfo | undefined { let stationInfo: ChargingStationInfo | undefined - if (stationInfoPersistentConfiguration) { + if (stationInfoPersistentConfiguration === true) { stationInfo = this.getConfigurationFromFile()?.stationInfo if (stationInfo != null) { delete stationInfo.infoHash + delete (stationInfo as ChargingStationTemplate).numberOfConnectors + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (stationInfo.templateIndex == null) { + stationInfo.templateIndex = this.index + } // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (stationInfo.templateName == null) { stationInfo.templateName = parse(this.templateFile).name } - if (stationInfo.autoStart == null) { - stationInfo.autoStart = true - } } } return stationInfo } - private getStationInfo (): ChargingStationInfo { - const defaultStationInfo = Constants.DEFAULT_STATION_INFO + private getStationInfo (options?: ChargingStationOptions): ChargingStationInfo { const stationInfoFromTemplate = this.getStationInfoFromTemplate() + options?.persistentConfiguration != null && + (stationInfoFromTemplate.stationInfoPersistentConfiguration = options.persistentConfiguration) const stationInfoFromFile = this.getStationInfoFromFile( stationInfoFromTemplate.stationInfoPersistentConfiguration ) @@ -1220,7 +1244,10 @@ export class ChargingStation extends EventEmitter { stationInfoFromFile != null && stationInfoFromFile.templateHash === stationInfoFromTemplate.templateHash ) { - return { ...defaultStationInfo, ...stationInfoFromFile } + return setChargingStationOptions( + { ...Constants.DEFAULT_STATION_INFO, ...stationInfoFromFile }, + options + ) } stationInfoFromFile != null && propagateSerialNumber( @@ -1228,7 +1255,10 @@ export class ChargingStation extends EventEmitter { stationInfoFromFile, stationInfoFromTemplate ) - return { ...defaultStationInfo, ...stationInfoFromTemplate } + return setChargingStationOptions( + { ...Constants.DEFAULT_STATION_INFO, ...stationInfoFromTemplate }, + options + ) } private saveStationInfo (): void { @@ -1243,7 +1273,7 @@ export class ChargingStation extends EventEmitter { throw new BaseError(errorMsg) } - private initialize (): void { + private initialize (options?: ChargingStationOptions): void { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const stationTemplate = this.getTemplateFromFile()! checkTemplate(stationTemplate, this.logPrefix(), this.templateFile) @@ -1261,11 +1291,11 @@ export class ChargingStation extends EventEmitter { } else { this.initializeConnectorsOrEvsesFromTemplate(stationTemplate) } - this.stationInfo = this.getStationInfo() + this.stationInfo = this.getStationInfo(options) if ( this.stationInfo.firmwareStatus === FirmwareStatus.Installing && - isNotEmptyString(this.stationInfo.firmwareVersion) && - isNotEmptyString(this.stationInfo.firmwareVersionPattern) + isNotEmptyString(this.stationInfo.firmwareVersionPattern) && + isNotEmptyString(this.stationInfo.firmwareVersion) ) { const patternGroup = this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ?? @@ -1301,7 +1331,7 @@ export class ChargingStation extends EventEmitter { this.bootNotificationRequest = bootNotificationRequest this.powerDivider = this.getPowerDivider() // OCPP configuration - this.ocppConfiguration = this.getOcppConfiguration() + this.ocppConfiguration = this.getOcppConfiguration(options?.persistentConfiguration) this.initializeOcppConfiguration() this.initializeOcppServices() if (this.stationInfo.autoRegister === true) { @@ -1692,14 +1722,11 @@ export class ChargingStation extends EventEmitter { } else { delete configurationData.configurationKey } - configurationData = merge( + configurationData = mergeDeepRight( configurationData, buildChargingStationAutomaticTransactionGeneratorConfiguration(this) ) - if ( - this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === false || - this.getAutomaticTransactionGeneratorConfiguration() == null - ) { + if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration !== true) { delete configurationData.automaticTransactionGenerator } if (this.connectors.size > 0) { @@ -1774,17 +1801,21 @@ export class ChargingStation extends EventEmitter { return this.getTemplateFromFile()?.Configuration } - private getOcppConfigurationFromFile (): ChargingStationOcppConfiguration | undefined { + private getOcppConfigurationFromFile ( + ocppPersistentConfiguration?: boolean + ): ChargingStationOcppConfiguration | undefined { const configurationKey = this.getConfigurationFromFile()?.configurationKey - if (this.stationInfo?.ocppPersistentConfiguration === true && Array.isArray(configurationKey)) { + if (ocppPersistentConfiguration === true && Array.isArray(configurationKey)) { return { configurationKey } } return undefined } - private getOcppConfiguration (): ChargingStationOcppConfiguration | undefined { + private getOcppConfiguration ( + ocppPersistentConfiguration: boolean | undefined = this.stationInfo?.ocppPersistentConfiguration + ): ChargingStationOcppConfiguration | undefined { let ocppConfiguration: ChargingStationOcppConfiguration | undefined = - this.getOcppConfigurationFromFile() + this.getOcppConfigurationFromFile(ocppPersistentConfiguration) if (ocppConfiguration == null) { ocppConfiguration = this.getOcppConfigurationFromTemplate() } @@ -1793,8 +1824,9 @@ export class ChargingStation extends EventEmitter { private async onOpen (): Promise { if (this.isWebSocketConnectionOpened()) { + this.emit(ChargingStationEvents.updated) logger.info( - `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded` + `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} succeeded` ) let registrationRetryCount = 0 if (!this.isRegistered()) { @@ -1848,13 +1880,14 @@ export class ChargingStation extends EventEmitter { this.emit(ChargingStationEvents.updated) } else { logger.warn( - `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed` + `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} failed` ) } } private onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): void { this.emit(ChargingStationEvents.disconnected) + this.emit(ChargingStationEvents.updated) switch (code) { // Normal close case WebSocketCloseEventStatusCode.CLOSE_NORMAL: @@ -1874,12 +1907,13 @@ export class ChargingStation extends EventEmitter { )}' and reason '${reason.toString()}'` ) this.started && - this.reconnect().catch(error => - logger.error(`${this.logPrefix()} Error while reconnecting:`, error) - ) + this.reconnect() + .then(() => { + this.emit(ChargingStationEvents.updated) + }) + .catch(error => logger.error(`${this.logPrefix()} Error while reconnecting:`, error)) break } - this.emit(ChargingStationEvents.updated) } private getCachedRequest ( @@ -2037,7 +2071,7 @@ export class ChargingStation extends EventEmitter { if (!(error instanceof OCPPError)) { logger.warn( `${this.logPrefix()} Error thrown at incoming OCPP command '${ - commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND + commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND // eslint-disable-next-line @typescript-eslint/no-base-to-string }' message '${data.toString()}' handling is not an OCPPError:`, error @@ -2045,7 +2079,7 @@ export class ChargingStation extends EventEmitter { } logger.error( `${this.logPrefix()} Incoming OCPP command '${ - commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND + commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND // eslint-disable-next-line @typescript-eslint/no-base-to-string }' message '${data.toString()}'${ this.requests.has(messageId) @@ -2093,7 +2127,8 @@ export class ChargingStation extends EventEmitter { } private getUseConnectorId0 (stationTemplate?: ChargingStationTemplate): boolean { - return stationTemplate?.useConnectorId0 ?? true + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return stationTemplate?.useConnectorId0 ?? Constants.DEFAULT_STATION_INFO.useConnectorId0! } private async stopRunningTransactions (reason?: StopTransactionReason): Promise { @@ -2152,8 +2187,12 @@ export class ChargingStation extends EventEmitter { } private getCurrentOutType (stationInfo?: ChargingStationInfo): CurrentType { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return (stationInfo ?? this.stationInfo!).currentOutType ?? CurrentType.AC + return ( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (stationInfo ?? this.stationInfo!).currentOutType ?? + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + Constants.DEFAULT_STATION_INFO.currentOutType! + ) } private getVoltageOut (stationInfo?: ChargingStationInfo): Voltage {