X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;ds=sidebyside;f=src%2Fcharging-station%2FChargingStation.ts;h=f79a59cea3943c2c9a79ca7c6cde8b37582b3d85;hb=a33026fe3cd6ce141efa55d27d35b4eb5c1a54b9;hp=7c4b098d3de215a7781d0909572b55e665518013;hpb=761fec7f45f0c6866cbf6a11a682fb949b64bf20;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 7c4b098d..f79a59ce 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -1,14 +1,14 @@ -// 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 { type FSWatcher, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { dirname, join } 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' @@ -56,7 +57,6 @@ import { type OCPPIncomingRequestService, type OCPPRequestService, buildMeterValue, - buildStatusNotificationRequest, buildTransactionEndMeterValue, getMessageTypeString, sendAndSetConnectorStatus @@ -74,6 +74,7 @@ import { ChargingStationEvents, type ChargingStationInfo, type ChargingStationOcppConfiguration, + type ChargingStationOptions, type ChargingStationTemplate, type ConnectorStatus, ConnectorStatusEnum, @@ -87,7 +88,6 @@ import { FirmwareStatus, type FirmwareStatusNotificationRequest, type FirmwareStatusNotificationResponse, - type FirmwareUpgrade, type HeartbeatRequest, type HeartbeatResponse, type IncomingRequest, @@ -107,8 +107,6 @@ import { type Response, StandardParametersKey, type Status, - type StatusNotificationRequest, - type StatusNotificationResponse, type StopTransactionReason, type StopTransactionRequest, type StopTransactionResponse, @@ -126,14 +124,18 @@ import { Configuration, Constants, DCElectricUtils, + buildAddedMessage, buildChargingStationAutomaticTransactionGeneratorConfiguration, buildConnectorsStatus, + buildDeletedMessage, buildEvsesStatus, buildStartedMessage, buildStoppedMessage, + buildTemplateName, buildUpdatedMessage, - cloneObject, + clone, convertToBoolean, + convertToDate, convertToInt, exponentialDelay, formatDurationMilliSeconds, @@ -156,22 +158,22 @@ import { export class ChargingStation extends EventEmitter { public readonly index: number public readonly templateFile: string - public stationInfo!: ChargingStationInfo + public stationInfo?: ChargingStationInfo 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 - public bootNotificationResponse!: BootNotificationResponse | undefined - public powerDivider!: number + public bootNotificationRequest?: BootNotificationRequest + public bootNotificationResponse?: BootNotificationResponse + public powerDivider?: number private stopping: boolean private configurationFile!: string private configurationFileHash!: string @@ -181,21 +183,23 @@ export class ChargingStation extends EventEmitter { private ocppIncomingRequestService!: OCPPIncomingRequestService private readonly messageBuffer: Set private configuredSupervisionUrl!: URL - private autoReconnectRetryCount: number - private templateFileWatcher!: FSWatcher | undefined + private wsConnectionRetried: boolean + private wsConnectionRetryCount: number + private templateFileWatcher?: FSWatcher private templateFileHash!: string private readonly sharedLRUCache: SharedLRUCache - private webSocketPingSetInterval?: NodeJS.Timeout + 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 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 +210,12 @@ 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.deleted, () => { + parentPort?.postMessage(buildDeletedMessage(this)) + }) this.on(ChargingStationEvents.started, () => { parentPort?.postMessage(buildStartedMessage(this)) }) @@ -215,31 +225,62 @@ 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.initialize(options) + + this.add() + + 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 && - 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(this.stationInfo.supervisionUrlOcppKey) && + isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value) + ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value : this.configuredSupervisionUrl.href - }/${this.stationInfo.chargingStationId}` + }/${this.stationInfo?.chargingStationId}` ) } public logPrefix = (): string => { - if (isNotEmptyString(this?.stationInfo?.chargingStationId)) { - return logPrefix(` ${this?.stationInfo?.chargingStationId} |`) + if ( + this instanceof ChargingStation && + this.stationInfo != null && + isNotEmptyString(this.stationInfo.chargingStationId) + ) { + return logPrefix(` ${this.stationInfo.chargingStationId} |`) } let stationTemplate: ChargingStationTemplate | undefined try { @@ -254,11 +295,12 @@ export class ChargingStation extends EventEmitter { public hasIdTags (): boolean { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return isNotEmptyArray(this.idTagsCache.getIdTags(getIdTagsFile(this.stationInfo)!)) + return isNotEmptyArray(this.idTagsCache.getIdTags(getIdTagsFile(this.stationInfo!)!)) } public getNumberOfPhases (stationInfo?: ChargingStationInfo): number { - const localStationInfo: ChargingStationInfo = stationInfo ?? this.stationInfo + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const localStationInfo = stationInfo ?? this.stationInfo! switch (this.getCurrentOutType(stationInfo)) { case CurrentType.AC: return localStationInfo.numberOfPhases ?? 3 @@ -268,23 +310,23 @@ export class ChargingStation extends EventEmitter { } public isWebSocketConnectionOpened (): boolean { - return this?.wsConnection?.readyState === WebSocket.OPEN + return this.wsConnection?.readyState === WebSocket.OPEN } public inUnknownState (): boolean { - return this?.bootNotificationResponse?.status == null + return this.bootNotificationResponse?.status == null } public inPendingState (): boolean { - return this?.bootNotificationResponse?.status === RegistrationStatusEnumType.PENDING + return this.bootNotificationResponse?.status === RegistrationStatusEnumType.PENDING } public inAcceptedState (): boolean { - return this?.bootNotificationResponse?.status === RegistrationStatusEnumType.ACCEPTED + return this.bootNotificationResponse?.status === RegistrationStatusEnumType.ACCEPTED } public inRejectedState (): boolean { - return this?.bootNotificationResponse?.status === RegistrationStatusEnumType.REJECTED + return this.bootNotificationResponse?.status === RegistrationStatusEnumType.REJECTED } public isRegistered (): boolean { @@ -345,10 +387,11 @@ export class ChargingStation extends EventEmitter { public getConnectorMaximumAvailablePower (connectorId: number): number { let connectorAmperageLimitationPowerLimit: number | undefined + const amperageLimitation = this.getAmperageLimitation() if ( - this.getAmperageLimitation() != null && + amperageLimitation != null && // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.getAmperageLimitation()! < this.stationInfo.maximumAmperage! + amperageLimitation < this.stationInfo!.maximumAmperage! ) { connectorAmperageLimitationPowerLimit = (this.stationInfo?.currentOutType === CurrentType.AC @@ -356,16 +399,16 @@ export class ChargingStation extends EventEmitter { this.getNumberOfPhases(), // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.stationInfo.voltageOut!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.getAmperageLimitation()! * + amperageLimitation * (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()) ) : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - DCElectricUtils.power(this.stationInfo.voltageOut!, this.getAmperageLimitation()!)) / - this.powerDivider + DCElectricUtils.power(this.stationInfo!.voltageOut!, amperageLimitation)) / + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.powerDivider! } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const connectorMaximumPower = this.stationInfo.maximumPower! / this.powerDivider + const connectorMaximumPower = this.stationInfo!.maximumPower! / this.powerDivider! const connectorChargingProfilesPowerLimit = getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId) return min( @@ -421,8 +464,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) { @@ -440,19 +485,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 { @@ -494,14 +538,14 @@ export class ChargingStation extends EventEmitter { public setSupervisionUrl (url: string): void { if ( this.stationInfo?.supervisionUrlOcppConfiguration === true && - isNotEmptyString(this.stationInfo?.supervisionUrlOcppKey) + 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 { - this.stationInfo.supervisionUrls = url - this.saveStationInfo() + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.stationInfo!.supervisionUrls = url this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl() + this.saveStationInfo() } } @@ -510,7 +554,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 @@ -554,21 +598,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` @@ -576,13 +621,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 @@ -591,11 +635,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 @@ -612,11 +656,34 @@ 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 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) { @@ -640,15 +707,21 @@ export class ChargingStation extends EventEmitter { } file have changed, reload` ) this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!) // Initialize this.initialize() - // 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) { - this.startAutomaticTransactionGenerator() + if ( + this.getAutomaticTransactionGeneratorConfiguration()?.enable === true && + ATGStarted === true + ) { + this.startAutomaticTransactionGenerator(undefined, true) } if (this.stationInfo?.enableStatistics === true) { this.performanceStatistics?.restart() @@ -676,7 +749,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 @@ -685,12 +761,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 { @@ -704,7 +779,7 @@ export class ChargingStation extends EventEmitter { public async reset (reason?: StopTransactionReason): Promise { await this.stop(reason) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await sleep(this.stationInfo.resetTime!) + await sleep(this.stationInfo!.resetTime!) this.initialize() this.start() } @@ -733,26 +808,24 @@ export class ChargingStation extends EventEmitter { if (!checkChargingStation(this, this.logPrefix())) { return } - if (this.stationInfo.supervisionUser != null && this.stationInfo.supervisionPassword != null) { + if (this.stationInfo?.supervisionUser != null && this.stationInfo.supervisionPassword != null) { options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}` } - if (params?.closeOpened === true) { + if (params.closeOpened === true) { this.closeWSConnection() } - if (params?.terminateOpened === true) { + if (params.terminateOpened === true) { this.terminateWSConnection() } 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, @@ -761,26 +834,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 { @@ -790,7 +860,9 @@ export class ChargingStation extends EventEmitter { } } - public getAutomaticTransactionGeneratorConfiguration (): AutomaticTransactionGeneratorConfiguration { + public getAutomaticTransactionGeneratorConfiguration (): + | AutomaticTransactionGeneratorConfiguration + | undefined { if (this.automaticTransactionGeneratorConfiguration == null) { let automaticTransactionGeneratorConfiguration: | AutomaticTransactionGeneratorConfiguration @@ -803,7 +875,7 @@ export class ChargingStation extends EventEmitter { stationConfiguration?.automaticTransactionGenerator != null ) { automaticTransactionGeneratorConfiguration = - stationConfiguration?.automaticTransactionGenerator + stationConfiguration.automaticTransactionGenerator } else { automaticTransactionGeneratorConfiguration = stationTemplate?.AutomaticTransactionGenerator } @@ -819,15 +891,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) @@ -835,8 +909,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 { @@ -853,14 +926,13 @@ export class ChargingStation extends EventEmitter { const transactionId = this.getConnectorStatus(connectorId)?.transactionId if ( this.stationInfo?.beginEndMeterValues === true && - this.stationInfo?.ocppStrictCompliance === true && - this.stationInfo?.outOfOrderEndMeterValues === false + this.stationInfo.ocppStrictCompliance === true && + this.stationInfo.outOfOrderEndMeterValues === false ) { const transactionEndMeterValue = buildTransactionEndMeterValue( this, connectorId, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.getEnergyActiveImportRegisterByTransactionId(transactionId!) + this.getEnergyActiveImportRegisterByTransactionId(transactionId) ) await this.ocppRequestService.requestHandler( this, @@ -877,8 +949,7 @@ export class ChargingStation extends EventEmitter { StopTransactionResponse >(this, RequestCommand.STOP_TRANSACTION, { transactionId, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId!, true), + meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true), ...(reason != null && { reason }) }) } @@ -942,14 +1013,14 @@ export class ChargingStation extends EventEmitter { if (this.hasEvses) { for (const evseStatus of this.evses.values()) { for (const connectorStatus of evseStatus.connectors.values()) { - if (connectorStatus?.reservation?.[filterKey] === value) { + if (connectorStatus.reservation?.[filterKey] === value) { return connectorStatus.reservation } } } } else { for (const connectorStatus of this.connectors.values()) { - if (connectorStatus?.reservation?.[filterKey] === value) { + if (connectorStatus.reservation?.[filterKey] === value) { return connectorStatus.reservation } } @@ -962,15 +1033,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 @@ -1084,40 +1154,38 @@ export class ChargingStation extends EventEmitter { private getStationInfoFromTemplate (): ChargingStationInfo { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const stationTemplate: ChargingStationTemplate = this.getTemplateFromFile()! + const stationTemplate = this.getTemplateFromFile()! checkTemplate(stationTemplate, this.logPrefix(), this.templateFile) const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation, this) warnTemplateKeysDeprecationOnce(stationTemplate, this.logPrefix(), this.templateFile) - if (stationTemplate?.Connectors != null) { + if (stationTemplate.Connectors != null) { checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile) } - const stationInfo: ChargingStationInfo = stationTemplateToStationInfo(stationTemplate) + const stationInfo = stationTemplateToStationInfo(stationTemplate) stationInfo.hashId = getHashId(this.index, stationTemplate) + stationInfo.templateIndex = this.index + stationInfo.templateName = buildTemplateName(this.templateFile) 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[] + if (isNotEmptyArray(stationTemplate.power)) { const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length) stationInfo.maximumPower = - stationTemplate?.powerUnit === PowerUnits.KILO_WATT + 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 + stationTemplate.powerUnit === PowerUnits.KILO_WATT + ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + stationTemplate.power! * 1000 : stationTemplate.power } stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo) - stationInfo.firmwareVersionPattern = - stationTemplate?.firmwareVersionPattern ?? Constants.SEMVER_PATTERN if ( + isNotEmptyString(stationInfo.firmwareVersionPattern) && 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 ${ @@ -1125,56 +1193,73 @@ export class ChargingStation extends EventEmitter { } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'` ) } - stationInfo.firmwareUpgrade = merge( + stationInfo.firmwareUpgrade = mergeDeepRight( { versionUpgrade: { step: 1 }, reset: true }, - stationTemplate?.firmwareUpgrade ?? {} + stationTemplate.firmwareUpgrade ?? {} ) - stationInfo.resetTime = - stationTemplate?.resetTime != null - ? secondsToMilliseconds(stationTemplate.resetTime) - : Constants.CHARGING_STATION_DEFAULT_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.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 = buildTemplateName(this.templateFile) + } } } return stationInfo } - private getStationInfo (): ChargingStationInfo { - const defaultStationInfo = Constants.DEFAULT_STATION_INFO - const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate() - const stationInfoFromFile: ChargingStationInfo | undefined = this.getStationInfoFromFile( - stationInfoFromTemplate?.stationInfoPersistentConfiguration + private getStationInfo (options?: ChargingStationOptions): ChargingStationInfo { + const stationInfoFromTemplate = this.getStationInfoFromTemplate() + options?.persistentConfiguration != null && + (stationInfoFromTemplate.stationInfoPersistentConfiguration = options.persistentConfiguration) + const stationInfoFromFile = this.getStationInfoFromFile( + stationInfoFromTemplate.stationInfoPersistentConfiguration ) // Priority: // 1. charging station info from template // 2. charging station info from configuration file - if (stationInfoFromFile?.templateHash === stationInfoFromTemplate.templateHash) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return { ...defaultStationInfo, ...stationInfoFromFile! } + if ( + stationInfoFromFile != null && + stationInfoFromFile.templateHash === stationInfoFromTemplate.templateHash + ) { + return setChargingStationOptions( + { ...Constants.DEFAULT_STATION_INFO, ...stationInfoFromFile }, + options + ) } stationInfoFromFile != null && propagateSerialNumber( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.getTemplateFromFile()!, + this.getTemplateFromFile(), stationInfoFromFile, stationInfoFromTemplate ) - return { ...defaultStationInfo, ...stationInfoFromTemplate } + return setChargingStationOptions( + { ...Constants.DEFAULT_STATION_INFO, ...stationInfoFromTemplate }, + options + ) } private saveStationInfo (): void { @@ -1189,7 +1274,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) @@ -1199,7 +1284,7 @@ export class ChargingStation extends EventEmitter { ) const stationConfiguration = this.getConfigurationFromFile() if ( - stationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash && + stationConfiguration?.stationInfo?.templateHash === stationTemplate.templateHash && (stationConfiguration?.connectorsStatus != null || stationConfiguration?.evsesStatus != null) ) { checkConfiguration(stationConfiguration, this.logPrefix(), this.configurationFile) @@ -1207,21 +1292,18 @@ 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: number | undefined = + 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] = ( @@ -1234,26 +1316,26 @@ export class ChargingStation extends EventEmitter { } this.saveStationInfo() this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl() - if (this.stationInfo?.enableStatistics === true) { + if (this.stationInfo.enableStatistics === true) { this.performanceStatistics = PerformanceStatistics.getInstance( this.stationInfo.hashId, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.stationInfo.chargingStationId!, + this.stationInfo.chargingStationId, this.configuredSupervisionUrl ) } - this.bootNotificationRequest = createBootNotificationRequest(this.stationInfo) + const bootNotificationRequest = createBootNotificationRequest(this.stationInfo) + if (bootNotificationRequest == null) { + const errorMsg = 'Error while creating boot notification request' + logger.error(`${this.logPrefix()} ${errorMsg}`) + throw new BaseError(errorMsg) + } + this.bootNotificationRequest = bootNotificationRequest this.powerDivider = this.getPowerDivider() // OCPP configuration - this.ocppConfiguration = this.getOcppConfiguration() + this.ocppConfiguration = this.getOcppConfiguration(options?.persistentConfiguration) 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) { + if (this.stationInfo.autoRegister === true) { this.bootNotificationResponse = { currentTime: new Date(), interval: millisecondsToSeconds(this.getHeartbeatInterval()), @@ -1295,35 +1377,29 @@ 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 + isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) && + 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 + isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) && + 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() @@ -1395,13 +1471,13 @@ export class ChargingStation extends EventEmitter { } private initializeConnectorsOrEvsesFromFile (configuration: ChargingStationConfiguration): void { - if (configuration?.connectorsStatus != null && configuration?.evsesStatus == null) { + 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) { + } 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), @@ -1414,7 +1490,7 @@ export class ChargingStation extends EventEmitter { ) }) } - } else if (configuration?.evsesStatus != null && configuration?.connectorsStatus != null) { + } else if (configuration.evsesStatus != null && configuration.connectorsStatus != null) { const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}` logger.error(`${this.logPrefix()} ${errorMsg}`) throw new BaseError(errorMsg) @@ -1426,11 +1502,11 @@ export class ChargingStation extends EventEmitter { } private initializeConnectorsOrEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void { - if (stationTemplate?.Connectors != null && stationTemplate?.Evses == null) { + if (stationTemplate.Connectors != null && stationTemplate.Evses == null) { this.initializeConnectorsFromTemplate(stationTemplate) - } else if (stationTemplate?.Evses != null && stationTemplate?.Connectors == null) { + } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) { this.initializeEvsesFromTemplate(stationTemplate) - } else if (stationTemplate?.Evses != null && stationTemplate?.Connectors != null) { + } else if (stationTemplate.Evses != null && stationTemplate.Connectors != null) { const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}` logger.error(`${this.logPrefix()} ${errorMsg}`) throw new BaseError(errorMsg) @@ -1442,52 +1518,53 @@ export class ChargingStation extends EventEmitter { } private initializeConnectorsFromTemplate (stationTemplate: ChargingStationTemplate): void { - if (stationTemplate?.Connectors == null && this.connectors.size === 0) { + if (stationTemplate.Connectors == null && this.connectors.size === 0) { const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined` logger.error(`${this.logPrefix()} ${errorMsg}`) throw new BaseError(errorMsg) } - if (stationTemplate?.Connectors?.[0] == null) { + if (stationTemplate.Connectors?.[0] == null) { logger.warn( `${this.logPrefix()} Charging station information from template ${ this.templateFile } with no connector id 0 configuration` ) } - if (stationTemplate?.Connectors != null) { + if (stationTemplate.Connectors != null) { const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } = checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile) const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM) .update( - `${JSON.stringify(stationTemplate?.Connectors)}${configuredMaxConnectors.toString()}` + `${JSON.stringify(stationTemplate.Connectors)}${configuredMaxConnectors.toString()}` ) .digest('hex') const connectorsConfigChanged = - this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash - if (this.connectors?.size === 0 || connectorsConfigChanged) { + this.connectors.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash + if (this.connectors.size === 0 || connectorsConfigChanged) { connectorsConfigChanged && this.connectors.clear() this.connectorsConfigurationHash = connectorsConfigHash if (templateMaxConnectors > 0) { for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) { if ( connectorId === 0 && - (stationTemplate?.Connectors?.[connectorId] == null || + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + (stationTemplate.Connectors[connectorId] == null || !this.getUseConnectorId0(stationTemplate)) ) { continue } const templateConnectorId = - connectorId > 0 && stationTemplate?.randomConnectors === true + connectorId > 0 && stationTemplate.randomConnectors === true ? getRandomInteger(templateMaxAvailableConnectors, 1) : connectorId - const connectorStatus = stationTemplate?.Connectors[templateConnectorId] + const connectorStatus = stationTemplate.Connectors[templateConnectorId] checkStationInfoConnectorStatus( templateConnectorId, connectorStatus, this.logPrefix(), this.templateFile ) - this.connectors.set(connectorId, cloneObject(connectorStatus)) + this.connectors.set(connectorId, clone(connectorStatus)) } initializeConnectorsMapStatus(this.connectors, this.logPrefix()) this.saveConnectorsStatus() @@ -1509,48 +1586,48 @@ export class ChargingStation extends EventEmitter { } private initializeEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void { - if (stationTemplate?.Evses == null && this.evses.size === 0) { + if (stationTemplate.Evses == null && this.evses.size === 0) { const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined` logger.error(`${this.logPrefix()} ${errorMsg}`) throw new BaseError(errorMsg) } - if (stationTemplate?.Evses?.[0] == null) { + if (stationTemplate.Evses?.[0] == null) { logger.warn( `${this.logPrefix()} Charging station information from template ${ this.templateFile } with no evse id 0 configuration` ) } - if (stationTemplate?.Evses?.[0]?.Connectors?.[0] == null) { + if (stationTemplate.Evses?.[0]?.Connectors[0] == null) { logger.warn( `${this.logPrefix()} Charging station information from template ${ this.templateFile } with evse id 0 with no connector id 0 configuration` ) } - if (Object.keys(stationTemplate?.Evses?.[0]?.Connectors as object).length > 1) { + 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 != null) { + if (stationTemplate.Evses != null) { const evsesConfigHash = 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 - if (this.evses?.size === 0 || evsesConfigChanged) { + this.evses.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash + if (this.evses.size === 0 || evsesConfigChanged) { evsesConfigChanged && this.evses.clear() this.evsesConfigurationHash = evsesConfigHash - const templateMaxEvses = getMaxNumberOfEvses(stationTemplate?.Evses) + const templateMaxEvses = getMaxNumberOfEvses(stationTemplate.Evses) if (templateMaxEvses > 0) { for (const evseKey in stationTemplate.Evses) { const evseId = convertToInt(evseKey) this.evses.set(evseId, { connectors: buildConnectorsMap( - stationTemplate?.Evses[evseKey]?.Connectors, + stationTemplate.Evses[evseKey].Connectors, this.logPrefix(), this.templateFile ), @@ -1628,15 +1705,12 @@ export class ChargingStation extends EventEmitter { if (!existsSync(dirname(this.configurationFile))) { mkdirSync(dirname(this.configurationFile), { recursive: true }) } + const configurationFromFile = this.getConfigurationFromFile() let configurationData: ChargingStationConfiguration = - this.getConfigurationFromFile() != null - ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - cloneObject(this.getConfigurationFromFile()!) + configurationFromFile != null + ? clone(configurationFromFile) : {} - if ( - this.stationInfo?.stationInfoPersistentConfiguration === true && - this.stationInfo != null - ) { + if (this.stationInfo?.stationInfoPersistentConfiguration === true) { configurationData.stationInfo = this.stationInfo } else { delete configurationData.stationInfo @@ -1645,18 +1719,15 @@ export class ChargingStation extends EventEmitter { this.stationInfo?.ocppPersistentConfiguration === true && Array.isArray(this.ocppConfiguration?.configurationKey) ) { - configurationData.configurationKey = this.ocppConfiguration?.configurationKey + configurationData.configurationKey = this.ocppConfiguration.configurationKey } 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) { @@ -1697,7 +1768,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, @@ -1731,17 +1802,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() } @@ -1750,8 +1825,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()) { @@ -1763,10 +1839,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( - this?.bootNotificationResponse?.interval != null + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + this.bootNotificationResponse?.interval != null ? secondsToMilliseconds(this.bootNotificationResponse.interval) : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL ) @@ -1774,7 +1858,7 @@ export class ChargingStation extends EventEmitter { } while ( !this.isRegistered() && // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (registrationRetryCount <= this.stationInfo.registrationMaxRetries! || + (registrationRetryCount <= this.stationInfo!.registrationMaxRetries! || this.stationInfo?.registrationMaxRetries === -1) ) } @@ -1784,21 +1868,27 @@ 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( - `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed` + `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} failed` ) } } - private async onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): Promise { + private onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): void { + this.emit(ChargingStationEvents.disconnected) + this.emit(ChargingStationEvents.updated) switch (code) { // Normal close case WebSocketCloseEventStatusCode.CLOSE_NORMAL: @@ -1808,7 +1898,7 @@ export class ChargingStation extends EventEmitter { code )}' and reason '${reason.toString()}'` ) - this.autoReconnectRetryCount = 0 + this.wsConnectionRetryCount = 0 break // Abnormal close default: @@ -1817,13 +1907,20 @@ export class ChargingStation extends EventEmitter { code )}' and reason '${reason.toString()}'` ) - this.started && (await this.reconnect()) + this.started && + this.reconnect() + .then(() => { + this.emit(ChargingStationEvents.updated) + }) + .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 @@ -1876,9 +1973,9 @@ export class ChargingStation extends EventEmitter { messageId )! logger.debug( - `${this.logPrefix()} << Command '${ - requestCommandName ?? Constants.UNKNOWN_COMMAND - }' received response payload: ${JSON.stringify(response)}` + `${this.logPrefix()} << Command '${requestCommandName}' received response payload: ${JSON.stringify( + response + )}` ) responseCallback(commandPayload, requestPayload) } @@ -1897,9 +1994,9 @@ export class ChargingStation extends EventEmitter { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)! logger.debug( - `${this.logPrefix()} << Command '${ - requestCommandName ?? Constants.UNKNOWN_COMMAND - }' received error response payload: ${JSON.stringify(errorResponse)}` + `${this.logPrefix()} << Command '${requestCommandName}' received error response payload: ${JSON.stringify( + errorResponse + )}` ) errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails)) } @@ -1945,11 +2042,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 @@ -1972,7 +2072,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 @@ -1980,11 +2080,11 @@ 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()}'${ - 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 @@ -2005,25 +2105,31 @@ 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 != 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 != null + ? Math.round(connectorStatus.energyActiveImportRegisterValue) + : undefined : connectorStatus?.energyActiveImportRegisterValue) ?? 0 ) } 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 { @@ -2051,8 +2157,7 @@ export class ChargingStation extends EventEmitter { private getConnectionTimeout (): number { if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) != null) { return convertToInt( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)!.value! ?? + getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)?.value ?? Constants.DEFAULT_CONNECTION_TIMEOUT ) } @@ -2069,7 +2174,7 @@ export class ChargingStation extends EventEmitter { private getMaximumAmperage (stationInfo?: ChargingStationInfo): number | undefined { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const maximumPower = (stationInfo ?? this.stationInfo).maximumPower! + const maximumPower = (stationInfo ?? this.stationInfo!).maximumPower! switch (this.getCurrentOutType(stationInfo)) { case CurrentType.AC: return ACElectricUtils.amperagePerPhaseFromPower( @@ -2083,12 +2188,18 @@ export class ChargingStation extends EventEmitter { } private getCurrentOutType (stationInfo?: ChargingStationInfo): CurrentType { - 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 { return ( - (stationInfo ?? this.stationInfo).voltageOut ?? + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (stationInfo ?? this.stationInfo!).voltageOut ?? getDefaultVoltageOut(this.getCurrentOutType(stationInfo), this.logPrefix(), this.templateFile) ) } @@ -2096,19 +2207,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 - ) / 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, @@ -2144,7 +2252,7 @@ export class ChargingStation extends EventEmitter { } } } - if (this.stationInfo.firmwareStatus === FirmwareStatus.Installing) { + if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) { await this.ocppRequestService.requestHandler< FirmwareStatusNotificationRequest, FirmwareStatusNotificationResponse @@ -2155,16 +2263,13 @@ export class ChargingStation extends EventEmitter { } // Start the ATG - if (this.getAutomaticTransactionGeneratorConfiguration().enable) { - this.startAutomaticTransactionGenerator() + if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) { + 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 @@ -2173,40 +2278,33 @@ 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 + delete connectorStatus.status } } } } 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 } } @@ -2214,14 +2312,14 @@ export class ChargingStation extends EventEmitter { } private startWebSocketPing (): void { - const webSocketPingInterval: number = + const webSocketPingInterval = getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null ? convertToInt( 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() } @@ -2231,7 +2329,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 @@ -2245,9 +2343,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 } } @@ -2258,9 +2356,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: @@ -2275,19 +2371,20 @@ 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) } const errorMsg = 'No supervision url(s) configured' logger.error(`${this.logPrefix()} ${errorMsg}`) - throw new BaseError(`${errorMsg}`) + throw new BaseError(errorMsg) } private stopHeartbeat (): void { @@ -2305,29 +2402,20 @@ 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) { - 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 = - reconnectDelay != null && reconnectDelay - reconnectDelayWithdraw > 0 - ? reconnectDelay - reconnectDelayWithdraw - : 0 + reconnectDelay - reconnectDelayWithdraw > 0 ? reconnectDelay - reconnectDelayWithdraw : 0 logger.error( `${this.logPrefix()} WebSocket connection retry in ${roundTo( reconnectDelay, @@ -2336,7 +2424,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( { @@ -2347,7 +2435,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})` ) }