X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2FChargingStation.ts;h=0f4bce43184793c9d3f7c221cb152f26dac5d9eb;hb=e9e43cff1ad9ba7f75e3ae64a53fceb596454a59;hp=a6721c5332ab09d7e1e4adf95ae627141653c6f2;hpb=97608fbd74bbcefe7733ab1a9f9612ae0811c438;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index a6721c53..0f4bce43 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -1,9 +1,9 @@ -// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved. +// Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved. 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,14 +123,16 @@ import { Configuration, Constants, DCElectricUtils, + buildAddedMessage, buildChargingStationAutomaticTransactionGeneratorConfiguration, buildConnectorsStatus, buildEvsesStatus, buildStartedMessage, buildStoppedMessage, buildUpdatedMessage, - cloneObject, + clone, convertToBoolean, + convertToDate, convertToInt, exponentialDelay, formatDurationMilliSeconds, @@ -169,8 +168,8 @@ export class ChargingStation extends EventEmitter { public performanceStatistics!: PerformanceStatistics | undefined public heartbeatSetInterval?: NodeJS.Timeout public ocppRequestService!: OCPPRequestService - public bootNotificationRequest!: BootNotificationRequest - public bootNotificationResponse!: BootNotificationResponse | undefined + public bootNotificationRequest?: BootNotificationRequest + public bootNotificationResponse?: BootNotificationResponse public powerDivider?: number private stopping: boolean private configurationFile!: string @@ -181,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 @@ -195,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() @@ -206,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)) }) @@ -215,8 +219,35 @@ 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.add() + + this.stationInfo?.autoStart === true && this.start() } public get hasEvses (): boolean { @@ -228,10 +259,8 @@ export class ChargingStation extends EventEmitter { `${ this.stationInfo?.supervisionUrlOcppConfiguration === true && isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) && - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!)?.value) - ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!)!.value + isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value) + ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value : this.configuredSupervisionUrl.href }/${this.stationInfo?.chargingStationId}` ) @@ -427,8 +456,10 @@ export class ChargingStation extends EventEmitter { return numberOfRunningTransactions } - public getConnectorIdByTransactionId (transactionId: number): number | undefined { - if (this.hasEvses) { + public getConnectorIdByTransactionId (transactionId: number | undefined): number | undefined { + if (transactionId == null) { + return undefined + } else if (this.hasEvses) { for (const evseStatus of this.evses.values()) { for (const [connectorId, connectorStatus] of evseStatus.connectors) { if (connectorStatus.transactionId === transactionId) { @@ -446,19 +477,18 @@ export class ChargingStation extends EventEmitter { } public getEnergyActiveImportRegisterByTransactionId ( - transactionId: number, + transactionId: number | undefined, rounded = false ): number { return this.getEnergyActiveImportRegister( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId)!)!, + this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId)!), rounded ) } public getEnergyActiveImportRegisterByConnectorId (connectorId: number, rounded = false): number { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId)!, rounded) + return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId), rounded) } public getAuthorizeRemoteTxRequests (): boolean { @@ -502,8 +532,7 @@ export class ChargingStation extends EventEmitter { this.stationInfo?.supervisionUrlOcppConfiguration === true && isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) ) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - setConfigurationKeyValue(this, this.stationInfo.supervisionUrlOcppKey!, url) + setConfigurationKeyValue(this, this.stationInfo.supervisionUrlOcppKey, url) } else { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.stationInfo!.supervisionUrls = url @@ -517,7 +546,7 @@ export class ChargingStation extends EventEmitter { this.heartbeatSetInterval = setInterval(() => { this.ocppRequestService .requestHandler(this, RequestCommand.HEARTBEAT) - .catch((error) => { + .catch(error => { logger.error( `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`, error @@ -561,21 +590,22 @@ export class ChargingStation extends EventEmitter { logger.error(`${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}`) return } - if (this.getConnectorStatus(connectorId) == null) { + const connectorStatus = this.getConnectorStatus(connectorId) + if (connectorStatus == null) { logger.error( `${this.logPrefix()} Trying to start MeterValues on non existing connector id ${connectorId}` ) return } - if (this.getConnectorStatus(connectorId)?.transactionStarted === false) { + if (connectorStatus.transactionStarted === false) { logger.error( `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started` ) return } else if ( - this.getConnectorStatus(connectorId)?.transactionStarted === true && - this.getConnectorStatus(connectorId)?.transactionId == null + connectorStatus.transactionStarted === true && + connectorStatus.transactionId == null ) { logger.error( `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id` @@ -583,13 +613,12 @@ export class ChargingStation extends EventEmitter { return } if (interval > 0) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.getConnectorStatus(connectorId)!.transactionSetInterval = setInterval(() => { + connectorStatus.transactionSetInterval = setInterval(() => { const meterValue = buildMeterValue( this, connectorId, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.getConnectorStatus(connectorId)!.transactionId!, + connectorStatus.transactionId!, interval ) this.ocppRequestService @@ -598,11 +627,11 @@ export class ChargingStation extends EventEmitter { RequestCommand.METER_VALUES, { connectorId, - transactionId: this.getConnectorStatus(connectorId)?.transactionId, + transactionId: connectorStatus.transactionId, meterValue: [meterValue] } ) - .catch((error) => { + .catch(error => { logger.error( `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`, error @@ -619,11 +648,16 @@ export class ChargingStation extends EventEmitter { } public stopMeterValues (connectorId: number): void { - if (this.getConnectorStatus(connectorId)?.transactionSetInterval != null) { - clearInterval(this.getConnectorStatus(connectorId)?.transactionSetInterval) + const connectorStatus = this.getConnectorStatus(connectorId) + if (connectorStatus?.transactionSetInterval != null) { + clearInterval(connectorStatus.transactionSetInterval) } } + private add (): void { + this.emit(ChargingStationEvents.added) + } + public start (): void { if (!this.started) { if (!this.starting) { @@ -652,10 +686,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 +723,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 @@ -768,26 +811,23 @@ export class ChargingStation extends EventEmitter { ) // Handle WebSocket message - this.wsConnection.on( - 'message', - this.onMessage.bind(this) as (this: WebSocket, data: RawData, isBinary: boolean) => void - ) + this.wsConnection.on('message', data => { + this.onMessage(data).catch(Constants.EMPTY_FUNCTION) + }) // Handle WebSocket error - this.wsConnection.on( - 'error', - this.onError.bind(this) as (this: WebSocket, error: Error) => void - ) + this.wsConnection.on('error', this.onError.bind(this)) // Handle WebSocket close - this.wsConnection.on( - 'close', - this.onClose.bind(this) as (this: WebSocket, code: number, reason: Buffer) => void - ) + this.wsConnection.on('close', this.onClose.bind(this)) // Handle WebSocket open - this.wsConnection.on('open', this.onOpen.bind(this) as (this: WebSocket) => void) + this.wsConnection.on('open', () => { + this.onOpen().catch(error => + logger.error(`${this.logPrefix()} Error while opening WebSocket connection:`, error) + ) + }) // Handle WebSocket ping - this.wsConnection.on('ping', this.onPing.bind(this) as (this: WebSocket, data: Buffer) => void) + this.wsConnection.on('ping', this.onPing.bind(this)) // Handle WebSocket pong - this.wsConnection.on('pong', this.onPong.bind(this) as (this: WebSocket, data: Buffer) => void) + this.wsConnection.on('pong', this.onPong.bind(this)) } public closeWSConnection (): void { @@ -828,15 +868,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)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - for (const connectorId of connectorIds!) { - this.automaticTransactionGenerator?.startConnector(connectorId) + for (const connectorId of connectorIds) { + this.automaticTransactionGenerator?.startConnector(connectorId, stopAbsoluteDuration) } } else { - this.automaticTransactionGenerator?.start() + this.automaticTransactionGenerator?.start(stopAbsoluteDuration) } this.saveAutomaticTransactionGeneratorConfiguration() this.emit(ChargingStationEvents.updated) @@ -844,8 +886,7 @@ export class ChargingStation extends EventEmitter { public stopAutomaticTransactionGenerator (connectorIds?: number[]): void { if (isNotEmptyArray(connectorIds)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - for (const connectorId of connectorIds!) { + for (const connectorId of connectorIds) { this.automaticTransactionGenerator?.stopConnector(connectorId) } } else { @@ -859,8 +900,7 @@ export class ChargingStation extends EventEmitter { connectorId: number, reason?: StopTransactionReason ): Promise { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const transactionId = this.getConnectorStatus(connectorId)!.transactionId! + const transactionId = this.getConnectorStatus(connectorId)?.transactionId if ( this.stationInfo?.beginEndMeterValues === true && this.stationInfo.ocppStrictCompliance === true && @@ -1101,22 +1141,23 @@ 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) stationInfo.voltageOut = this.getVoltageOut(stationInfo) if (isNotEmptyArray(stationTemplate.power)) { - stationTemplate.power = stationTemplate.power as number[] const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length) stationInfo.maximumPower = stationTemplate.powerUnit === PowerUnits.KILO_WATT ? stationTemplate.power[powerArrayRandomIndex] * 1000 : stationTemplate.power[powerArrayRandomIndex] } else { - stationTemplate.power = stationTemplate.power as number stationInfo.maximumPower = stationTemplate.powerUnit === PowerUnits.KILO_WATT - ? stationTemplate.power * 1000 + ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + stationTemplate.power! * 1000 : stationTemplate.power } stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo) @@ -1124,8 +1165,7 @@ export class ChargingStation extends EventEmitter { stationTemplate.firmwareVersionPattern ?? Constants.SEMVER_PATTERN if ( isNotEmptyString(stationInfo.firmwareVersion) && - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - !new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion!) + !new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion) ) { logger.warn( `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${ @@ -1145,7 +1185,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 } @@ -1157,6 +1197,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 @@ -1224,13 +1271,10 @@ export class ChargingStation extends EventEmitter { ) { const patternGroup = this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ?? - this.stationInfo.firmwareVersion?.split('.').length - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const match = new RegExp(this.stationInfo.firmwareVersionPattern!) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - .exec(this.stationInfo.firmwareVersion!) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - ?.slice(1, patternGroup! + 1) + this.stationInfo.firmwareVersion.split('.').length + const match = new RegExp(this.stationInfo.firmwareVersionPattern) + .exec(this.stationInfo.firmwareVersion) + ?.slice(1, patternGroup + 1) if (match != null) { const patchLevelIndex = match.length - 1 match[patchLevelIndex] = ( @@ -1262,11 +1306,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(), @@ -1310,37 +1349,31 @@ export class ChargingStation extends EventEmitter { if ( this.stationInfo?.supervisionUrlOcppConfiguration === true && isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) && - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!) == null + getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) == null ) { addConfigurationKey( this, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.stationInfo.supervisionUrlOcppKey!, + this.stationInfo.supervisionUrlOcppKey, this.configuredSupervisionUrl.href, { reboot: true } ) } else if ( this.stationInfo?.supervisionUrlOcppConfiguration === false && isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) && - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!) != null + getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) != null ) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey!, { save: false }) + deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey, { save: false }) } if ( isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) && - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey!) == null + getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) == null ) { addConfigurationKey( this, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.stationInfo!.amperageLimitationOcppKey!, + this.stationInfo.amperageLimitationOcppKey, // prettier-ignore // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (this.stationInfo!.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo)).toString() + (this.stationInfo.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo)).toString() ) } if (getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles) == null) { @@ -1411,11 +1444,11 @@ export class ChargingStation extends EventEmitter { private initializeConnectorsOrEvsesFromFile (configuration: ChargingStationConfiguration): void { if (configuration.connectorsStatus != null && configuration.evsesStatus == null) { for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) { - this.connectors.set(connectorId, cloneObject(connectorStatus)) + this.connectors.set(connectorId, clone(connectorStatus)) } } else if (configuration.evsesStatus != null && configuration.connectorsStatus == null) { for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) { - const evseStatus = cloneObject(evseStatusConfiguration) + const evseStatus = clone(evseStatusConfiguration) delete evseStatus.connectorsStatus this.evses.set(evseId, { ...(evseStatus as EvseStatus), @@ -1502,7 +1535,7 @@ export class ChargingStation extends EventEmitter { this.logPrefix(), this.templateFile ) - this.connectors.set(connectorId, cloneObject(connectorStatus)) + this.connectors.set(connectorId, clone(connectorStatus)) } initializeConnectorsMapStatus(this.connectors, this.logPrefix()) this.saveConnectorsStatus() @@ -1646,7 +1679,7 @@ export class ChargingStation extends EventEmitter { const configurationFromFile = this.getConfigurationFromFile() let configurationData: ChargingStationConfiguration = configurationFromFile != null - ? cloneObject(configurationFromFile) + ? clone(configurationFromFile) : {} if (this.stationInfo?.stationInfoPersistentConfiguration === true) { configurationData.stationInfo = this.stationInfo @@ -1709,7 +1742,7 @@ export class ChargingStation extends EventEmitter { this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash) this.sharedLRUCache.setChargingStationConfiguration(configurationData) this.configurationFileHash = configurationHash - }).catch((error) => { + }).catch(error => { handleFileException( this.configurationFile, FileType.ChargingStationConfiguration, @@ -1775,11 +1808,18 @@ export class ChargingStation extends EventEmitter { >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, { skipBufferingOnError: true }) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (this.bootNotificationResponse?.currentTime != null) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.bootNotificationResponse.currentTime = convertToDate( + this.bootNotificationResponse.currentTime + )! + } if (!this.isRegistered()) { this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount await sleep( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - this.bootNotificationResponse.interval != null + this.bootNotificationResponse?.interval != null ? secondsToMilliseconds(this.bootNotificationResponse.interval) : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL ) @@ -1797,12 +1837,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.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( @@ -1811,7 +1855,8 @@ export class ChargingStation extends EventEmitter { } } - private async onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): Promise { + private onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): void { + this.emit(ChargingStationEvents.disconnected) switch (code) { // Normal close case WebSocketCloseEventStatusCode.CLOSE_NORMAL: @@ -1821,7 +1866,7 @@ export class ChargingStation extends EventEmitter { code )}' and reason '${reason.toString()}'` ) - this.autoReconnectRetryCount = 0 + this.wsConnectionRetryCount = 0 break // Abnormal close default: @@ -1830,13 +1875,19 @@ export class ChargingStation extends EventEmitter { code )}' and reason '${reason.toString()}'` ) - this.started && (await this.reconnect()) + 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 @@ -1958,11 +2009,14 @@ export class ChargingStation extends EventEmitter { ) } } catch (error) { + if (!Array.isArray(request)) { + logger.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error) + return + } let commandName: IncomingRequestCommand | undefined let requestCommandName: RequestCommand | IncomingRequestCommand | undefined let errorCallback: ErrorCallback - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const [, messageId] = request! + const [, messageId] = request switch (messageType) { case MessageType.CALL_MESSAGE: [, , commandName] = request as IncomingRequest @@ -1996,8 +2050,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 @@ -2018,20 +2072,25 @@ export class ChargingStation extends EventEmitter { logger.error(`${this.logPrefix()} WebSocket error:`, error) } - private getEnergyActiveImportRegister (connectorStatus: ConnectorStatus, rounded = false): number { + private getEnergyActiveImportRegister ( + connectorStatus: ConnectorStatus | undefined, + rounded = false + ): number { if (this.stationInfo?.meteringPerTransaction === true) { return ( (rounded - ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue!) - : connectorStatus.transactionEnergyActiveImportRegisterValue) ?? 0 + ? connectorStatus?.transactionEnergyActiveImportRegisterValue != null + ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue) + : undefined + : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0 ) } return ( (rounded - ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - Math.round(connectorStatus.energyActiveImportRegisterValue!) - : connectorStatus.energyActiveImportRegisterValue) ?? 0 + ? connectorStatus?.energyActiveImportRegisterValue != null + ? Math.round(connectorStatus.energyActiveImportRegisterValue) + : undefined + : connectorStatus?.energyActiveImportRegisterValue) ?? 0 ) } @@ -2110,20 +2169,16 @@ export class ChargingStation extends EventEmitter { private getAmperageLimitation (): number | undefined { if ( isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) && - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - getConfigurationKey(this, this.stationInfo!.amperageLimitationOcppKey!) != null + getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) != null ) { return ( - convertToInt( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - getConfigurationKey(this, this.stationInfo!.amperageLimitationOcppKey!)!.value - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - ) / getAmperageLimitationUnitDivider(this.stationInfo!) + convertToInt(getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey)?.value) / + getAmperageLimitationUnitDivider(this.stationInfo) ) } } - private async startMessageSequence (): Promise { + private async startMessageSequence (ATGStopAbsoluteDuration?: boolean): Promise { if (this.stationInfo?.autoRegister === true) { await this.ocppRequestService.requestHandler< BootNotificationRequest, @@ -2171,15 +2226,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 @@ -2188,24 +2240,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 } @@ -2214,14 +2266,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 } } @@ -2235,8 +2280,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() } @@ -2246,7 +2291,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 @@ -2260,9 +2305,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 } } @@ -2273,9 +2318,7 @@ export class ChargingStation extends EventEmitter { let configuredSupervisionUrlIndex: number switch (Configuration.getSupervisionUrlDistribution()) { case SupervisionUrlDistribution.RANDOM: - configuredSupervisionUrlIndex = Math.floor( - secureRandom() * (supervisionUrls as string[]).length - ) + configuredSupervisionUrlIndex = Math.floor(secureRandom() * supervisionUrls.length) break case SupervisionUrlDistribution.ROUND_ROBIN: case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY: @@ -2290,12 +2333,13 @@ export class ChargingStation extends EventEmitter { SupervisionUrlDistribution.CHARGING_STATION_AFFINITY }` ) - configuredSupervisionUrlIndex = (this.index - 1) % (supervisionUrls as string[]).length + configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length break } - configuredSupervisionUrl = (supervisionUrls as string[])[configuredSupervisionUrlIndex] + configuredSupervisionUrl = supervisionUrls[configuredSupervisionUrlIndex] } else { - configuredSupervisionUrl = supervisionUrls as string + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + configuredSupervisionUrl = supervisionUrls! } if (isNotEmptyString(configuredSupervisionUrl)) { return new URL(configuredSupervisionUrl) @@ -2320,23 +2364,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 = @@ -2349,7 +2386,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( { @@ -2360,7 +2397,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})` ) }