X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;ds=inline;f=src%2Fcharging-station%2FChargingStation.ts;h=c8eaf5e44bc70e18844a37ac79390db7a8b845bc;hb=244c1396e337032577839fa13e9191d5e943864f;hp=e9d94651d1d2dd57fccd4440708663206592a4d5;hpb=5a15db9001b93665ab7581d2974c5f01a9e0fbe3;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index e9d94651..c8eaf5e4 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -3,7 +3,7 @@ import { createHash } from 'node:crypto' import { EventEmitter } from 'node:events' import { type FSWatcher, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' -import { dirname, join } from 'node:path' +import { dirname, join, parse } from 'node:path' import { URL } from 'node:url' import { parentPort } from 'node:worker_threads' @@ -56,7 +56,6 @@ import { type OCPPIncomingRequestService, type OCPPRequestService, buildMeterValue, - buildStatusNotificationRequest, buildTransactionEndMeterValue, getMessageTypeString, sendAndSetConnectorStatus @@ -107,8 +106,6 @@ import { type Response, StandardParametersKey, type Status, - type StatusNotificationRequest, - type StatusNotificationResponse, type StopTransactionReason, type StopTransactionRequest, type StopTransactionResponse, @@ -126,6 +123,7 @@ import { Configuration, Constants, DCElectricUtils, + buildAddedMessage, buildChargingStationAutomaticTransactionGeneratorConfiguration, buildConnectorsStatus, buildEvsesStatus, @@ -182,11 +180,12 @@ export class ChargingStation extends EventEmitter { private ocppIncomingRequestService!: OCPPIncomingRequestService private readonly messageBuffer: Set private configuredSupervisionUrl!: URL - private autoReconnectRetryCount: number + private wsConnectionRetried: boolean + private wsConnectionRetryCount: number private templateFileWatcher!: FSWatcher | undefined private templateFileHash!: string private readonly sharedLRUCache: SharedLRUCache - private webSocketPingSetInterval?: NodeJS.Timeout + private wsPingSetInterval?: NodeJS.Timeout private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel private flushMessageBufferSetInterval?: NodeJS.Timeout @@ -196,7 +195,8 @@ export class ChargingStation extends EventEmitter { this.starting = false this.stopping = false this.wsConnection = null - this.autoReconnectRetryCount = 0 + this.wsConnectionRetried = false + this.wsConnectionRetryCount = 0 this.index = index this.templateFile = templateFile this.connectors = new Map() @@ -207,6 +207,9 @@ export class ChargingStation extends EventEmitter { this.idTagsCache = IdTagsCache.getInstance() this.chargingStationWorkerBroadcastChannel = new ChargingStationWorkerBroadcastChannel(this) + this.on(ChargingStationEvents.added, () => { + parentPort?.postMessage(buildAddedMessage(this)) + }) this.on(ChargingStationEvents.started, () => { parentPort?.postMessage(buildStartedMessage(this)) }) @@ -216,8 +219,33 @@ export class ChargingStation extends EventEmitter { this.on(ChargingStationEvents.updated, () => { parentPort?.postMessage(buildUpdatedMessage(this)) }) + this.on(ChargingStationEvents.accepted, () => { + this.startMessageSequence( + this.wsConnectionRetried + ? true + : this.getAutomaticTransactionGeneratorConfiguration()?.stopAbsoluteDuration + ).catch(error => { + logger.error(`${this.logPrefix()} Error while starting the message sequence:`, error) + }) + this.wsConnectionRetried = false + }) + this.on(ChargingStationEvents.rejected, () => { + this.wsConnectionRetried = false + }) + this.on(ChargingStationEvents.disconnected, () => { + try { + this.internalStopMessageSequence() + } catch (error) { + logger.error( + `${this.logPrefix()} Error while stopping the internal message sequence:`, + error + ) + } + }) this.initialize() + + this.stationInfo?.autoStart === true && this.start() } public get hasEvses (): boolean { @@ -624,6 +652,10 @@ export class ChargingStation extends EventEmitter { } } + public add (): void { + this.emit(ChargingStationEvents.added) + } + public start (): void { if (!this.started) { if (!this.starting) { @@ -652,10 +684,16 @@ export class ChargingStation extends EventEmitter { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!) // Restart the ATG - this.stopAutomaticTransactionGenerator() + const ATGStarted = this.automaticTransactionGenerator?.started + if (ATGStarted === true) { + this.stopAutomaticTransactionGenerator() + } delete this.automaticTransactionGeneratorConfiguration - if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) { - this.startAutomaticTransactionGenerator() + if ( + this.getAutomaticTransactionGeneratorConfiguration()?.enable === true && + ATGStarted === true + ) { + this.startAutomaticTransactionGenerator(undefined, true) } if (this.stationInfo?.enableStatistics === true) { this.performanceStatistics?.restart() @@ -683,7 +721,10 @@ export class ChargingStation extends EventEmitter { } } - public async stop (reason?: StopTransactionReason, stopTransactions?: boolean): Promise { + public async stop ( + reason?: StopTransactionReason, + stopTransactions = this.stationInfo?.stopTransactionsOnStopped + ): Promise { if (this.started) { if (!this.stopping) { this.stopping = true @@ -825,14 +866,17 @@ export class ChargingStation extends EventEmitter { return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses } - public startAutomaticTransactionGenerator (connectorIds?: number[]): void { + public startAutomaticTransactionGenerator ( + connectorIds?: number[], + stopAbsoluteDuration?: boolean + ): void { this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this) if (isNotEmptyArray(connectorIds)) { for (const connectorId of connectorIds) { - this.automaticTransactionGenerator?.startConnector(connectorId) + this.automaticTransactionGenerator?.startConnector(connectorId, stopAbsoluteDuration) } } else { - this.automaticTransactionGenerator?.start() + this.automaticTransactionGenerator?.start(stopAbsoluteDuration) } this.saveAutomaticTransactionGeneratorConfiguration() this.emit(ChargingStationEvents.updated) @@ -1095,6 +1139,8 @@ export class ChargingStation extends EventEmitter { } const stationInfo = stationTemplateToStationInfo(stationTemplate) stationInfo.hashId = getHashId(this.index, stationTemplate) + stationInfo.autoStart = stationTemplate.autoStart ?? true + stationInfo.templateName = parse(this.templateFile).name stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate) stationInfo.ocppVersion = stationTemplate.ocppVersion ?? OCPPVersion.VERSION_16 createSerialNumber(stationTemplate, stationInfo) @@ -1137,7 +1183,7 @@ export class ChargingStation extends EventEmitter { stationInfo.resetTime = stationTemplate.resetTime != null ? secondsToMilliseconds(stationTemplate.resetTime) - : Constants.CHARGING_STATION_DEFAULT_RESET_TIME + : Constants.DEFAULT_CHARGING_STATION_RESET_TIME return stationInfo } @@ -1149,6 +1195,13 @@ export class ChargingStation extends EventEmitter { stationInfo = this.getConfigurationFromFile()?.stationInfo if (stationInfo != null) { delete stationInfo.infoHash + // 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 @@ -1251,11 +1304,6 @@ export class ChargingStation extends EventEmitter { this.ocppConfiguration = this.getOcppConfiguration() this.initializeOcppConfiguration() this.initializeOcppServices() - this.once(ChargingStationEvents.accepted, () => { - this.startMessageSequence().catch(error => { - logger.error(`${this.logPrefix()} Error while starting the message sequence:`, error) - }) - }) if (this.stationInfo.autoRegister === true) { this.bootNotificationResponse = { currentTime: new Date(), @@ -1787,13 +1835,16 @@ export class ChargingStation extends EventEmitter { this.emit(ChargingStationEvents.accepted) } } else { + if (this.inRejectedState()) { + this.emit(ChargingStationEvents.rejected) + } logger.error( `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${ this.stationInfo?.registrationMaxRetries })` ) } - this.autoReconnectRetryCount = 0 + this.wsConnectionRetryCount = 0 this.emit(ChargingStationEvents.updated) } else { logger.warn( @@ -1803,6 +1854,7 @@ export class ChargingStation extends EventEmitter { } private onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): void { + this.emit(ChargingStationEvents.disconnected) switch (code) { // Normal close case WebSocketCloseEventStatusCode.CLOSE_NORMAL: @@ -1812,7 +1864,7 @@ export class ChargingStation extends EventEmitter { code )}' and reason '${reason.toString()}'` ) - this.autoReconnectRetryCount = 0 + this.wsConnectionRetryCount = 0 break // Abnormal close default: @@ -1821,13 +1873,19 @@ export class ChargingStation extends EventEmitter { code )}' and reason '${reason.toString()}'` ) - this.started && this.reconnect().catch(Constants.EMPTY_FUNCTION) + this.started && + this.reconnect().catch(error => + logger.error(`${this.logPrefix()} Error while reconnecting:`, error) + ) break } this.emit(ChargingStationEvents.updated) } - private getCachedRequest (messageType: MessageType, messageId: string): CachedRequest | undefined { + private getCachedRequest ( + messageType: MessageType | undefined, + messageId: string + ): CachedRequest | undefined { const cachedRequest = this.requests.get(messageId) if (Array.isArray(cachedRequest)) { return cachedRequest @@ -1990,8 +2048,8 @@ export class ChargingStation extends EventEmitter { commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND // eslint-disable-next-line @typescript-eslint/no-base-to-string }' message '${data.toString()}'${ - messageType !== MessageType.CALL_MESSAGE - ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'` + this.requests.has(messageId) + ? ` matching cached request '${JSON.stringify(this.getCachedRequest(messageType, messageId))}'` : '' } processing error:`, error @@ -2118,7 +2176,7 @@ export class ChargingStation extends EventEmitter { } } - private async startMessageSequence (): Promise { + private async startMessageSequence (ATGStopAbsoluteDuration?: boolean): Promise { if (this.stationInfo?.autoRegister === true) { await this.ocppRequestService.requestHandler< BootNotificationRequest, @@ -2166,15 +2224,12 @@ export class ChargingStation extends EventEmitter { // Start the ATG if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) { - this.startAutomaticTransactionGenerator() + this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration) } this.flushMessageBuffer() } - private async stopMessageSequence ( - reason?: StopTransactionReason, - stopTransactions = this.stationInfo?.stopTransactionsOnStopped - ): Promise { + private internalStopMessageSequence (): void { // Stop WebSocket ping this.stopWebSocketPing() // Stop heartbeat @@ -2183,24 +2238,24 @@ export class ChargingStation extends EventEmitter { if (this.automaticTransactionGenerator?.started === true) { this.stopAutomaticTransactionGenerator() } + } + + private async stopMessageSequence ( + reason?: StopTransactionReason, + stopTransactions?: boolean + ): Promise { + this.internalStopMessageSequence() // Stop ongoing transactions stopTransactions === true && (await this.stopRunningTransactions(reason)) if (this.hasEvses) { for (const [evseId, evseStatus] of this.evses) { if (evseId > 0) { for (const [connectorId, connectorStatus] of evseStatus.connectors) { - await this.ocppRequestService.requestHandler< - StatusNotificationRequest, - StatusNotificationResponse - >( + await sendAndSetConnectorStatus( this, - RequestCommand.STATUS_NOTIFICATION, - buildStatusNotificationRequest( - this, - connectorId, - ConnectorStatusEnum.Unavailable, - evseId - ) + connectorId, + ConnectorStatusEnum.Unavailable, + evseId ) delete connectorStatus.status } @@ -2209,14 +2264,7 @@ export class ChargingStation extends EventEmitter { } else { for (const connectorId of this.connectors.keys()) { if (connectorId > 0) { - await this.ocppRequestService.requestHandler< - StatusNotificationRequest, - StatusNotificationResponse - >( - this, - RequestCommand.STATUS_NOTIFICATION, - buildStatusNotificationRequest(this, connectorId, ConnectorStatusEnum.Unavailable) - ) + await sendAndSetConnectorStatus(this, connectorId, ConnectorStatusEnum.Unavailable) delete this.getConnectorStatus(connectorId)?.status } } @@ -2230,8 +2278,8 @@ export class ChargingStation extends EventEmitter { getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value ) : 0 - if (webSocketPingInterval > 0 && this.webSocketPingSetInterval == null) { - this.webSocketPingSetInterval = setInterval(() => { + if (webSocketPingInterval > 0 && this.wsPingSetInterval == null) { + this.wsPingSetInterval = setInterval(() => { if (this.isWebSocketConnectionOpened()) { this.wsConnection?.ping() } @@ -2241,7 +2289,7 @@ export class ChargingStation extends EventEmitter { webSocketPingInterval )}` ) - } else if (this.webSocketPingSetInterval != null) { + } else if (this.wsPingSetInterval != null) { logger.info( `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds( webSocketPingInterval @@ -2255,9 +2303,9 @@ export class ChargingStation extends EventEmitter { } private stopWebSocketPing (): void { - if (this.webSocketPingSetInterval != null) { - clearInterval(this.webSocketPingSetInterval) - delete this.webSocketPingSetInterval + if (this.wsPingSetInterval != null) { + clearInterval(this.wsPingSetInterval) + delete this.wsPingSetInterval } } @@ -2314,23 +2362,16 @@ export class ChargingStation extends EventEmitter { } private async reconnect (): Promise { - // Stop WebSocket ping - this.stopWebSocketPing() - // Stop heartbeat - this.stopHeartbeat() - // Stop the ATG if needed - if (this.getAutomaticTransactionGeneratorConfiguration()?.stopOnConnectionFailure === true) { - this.stopAutomaticTransactionGenerator() - } if ( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.autoReconnectRetryCount < this.stationInfo!.autoReconnectMaxRetries! || + this.wsConnectionRetryCount < this.stationInfo!.autoReconnectMaxRetries! || this.stationInfo?.autoReconnectMaxRetries === -1 ) { - ++this.autoReconnectRetryCount + this.wsConnectionRetried = true + ++this.wsConnectionRetryCount const reconnectDelay = this.stationInfo?.reconnectExponentialDelay === true - ? exponentialDelay(this.autoReconnectRetryCount) + ? exponentialDelay(this.wsConnectionRetryCount) : secondsToMilliseconds(this.getConnectionTimeout()) const reconnectDelayWithdraw = 1000 const reconnectTimeout = @@ -2343,7 +2384,7 @@ export class ChargingStation extends EventEmitter { ) await sleep(reconnectDelay) logger.error( - `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}` + `${this.logPrefix()} WebSocket connection retry #${this.wsConnectionRetryCount.toString()}` ) this.openWSConnection( { @@ -2354,7 +2395,7 @@ export class ChargingStation extends EventEmitter { } else if (this.stationInfo?.autoReconnectMaxRetries !== -1) { logger.error( `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${ - this.autoReconnectRetryCount + this.wsConnectionRetryCount }) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries})` ) }