From fa7bccf4a465d2156c43a3b5f33f0b521da52dc3 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Mon, 23 May 2022 21:02:28 +0200 Subject: [PATCH] Untangle charging station info from charging station template data structure MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Should fix https://github.com/jerome-benoit/emobility-charging-stations-simulator/issues/222 Signed-off-by: Jérôme Benoit --- .../AutomaticTransactionGenerator.ts | 44 ++- src/charging-station/ChargingStation.ts | 369 ++++++++---------- src/charging-station/ChargingStationUtils.ts | 199 ++++++++-- .../ocpp/1.6/OCPP16ResponseService.ts | 4 +- .../ocpp/1.6/OCPP16ServiceUtils.ts | 6 +- src/types/AutomaticTransactionGenerator.ts | 12 + src/types/ChargingStationInfo.ts | 12 +- src/types/ChargingStationTemplate.ts | 15 +- 8 files changed, 362 insertions(+), 299 deletions(-) diff --git a/src/charging-station/AutomaticTransactionGenerator.ts b/src/charging-station/AutomaticTransactionGenerator.ts index b806d5cf..fedda8ef 100644 --- a/src/charging-station/AutomaticTransactionGenerator.ts +++ b/src/charging-station/AutomaticTransactionGenerator.ts @@ -10,6 +10,10 @@ import { StopTransactionRequest, StopTransactionResponse, } from '../types/ocpp/Transaction'; +import { + AutomaticTransactionGeneratorConfiguration, + Status, +} from '../types/AutomaticTransactionGenerator'; import { MeterValuesRequest, RequestCommand } from '../types/ocpp/Requests'; import type ChargingStation from './ChargingStation'; @@ -17,7 +21,6 @@ import Constants from '../utils/Constants'; import { MeterValuesResponse } from '../types/ocpp/Responses'; import { OCPP16ServiceUtils } from './ocpp/1.6/OCPP16ServiceUtils'; import PerformanceStatistics from '../performance/PerformanceStatistics'; -import { Status } from '../types/AutomaticTransactionGenerator'; import Utils from '../utils/Utils'; import logger from '../utils/Logger'; @@ -27,22 +30,33 @@ export default class AutomaticTransactionGenerator { AutomaticTransactionGenerator >(); + public readonly configuration: AutomaticTransactionGeneratorConfiguration; public started: boolean; private readonly chargingStation: ChargingStation; private readonly connectorsStatus: Map; - private constructor(chargingStation: ChargingStation) { + private constructor( + automaticTransactionGeneratorConfiguration: AutomaticTransactionGeneratorConfiguration, + chargingStation: ChargingStation + ) { + this.configuration = automaticTransactionGeneratorConfiguration; this.chargingStation = chargingStation; this.connectorsStatus = new Map(); this.stopConnectors(); this.started = false; } - public static getInstance(chargingStation: ChargingStation): AutomaticTransactionGenerator { + public static getInstance( + automaticTransactionGeneratorConfiguration: AutomaticTransactionGeneratorConfiguration, + chargingStation: ChargingStation + ): AutomaticTransactionGenerator { if (!AutomaticTransactionGenerator.instances.has(chargingStation.hashId)) { AutomaticTransactionGenerator.instances.set( chargingStation.hashId, - new AutomaticTransactionGenerator(chargingStation) + new AutomaticTransactionGenerator( + automaticTransactionGeneratorConfiguration, + chargingStation + ) ); } return AutomaticTransactionGenerator.instances.get(chargingStation.hashId); @@ -140,19 +154,15 @@ export default class AutomaticTransactionGenerator { } const wait = Utils.getRandomInteger( - this.chargingStation.stationInfo.AutomaticTransactionGenerator - .maxDelayBetweenTwoTransactions, - this.chargingStation.stationInfo.AutomaticTransactionGenerator - .minDelayBetweenTwoTransactions + this.configuration.maxDelayBetweenTwoTransactions, + this.configuration.minDelayBetweenTwoTransactions ) * 1000; logger.info( this.logPrefix(connectorId) + ' waiting for ' + Utils.formatDurationMilliSeconds(wait) ); await Utils.sleep(wait); const start = Utils.secureRandom(); - if ( - start < this.chargingStation.stationInfo.AutomaticTransactionGenerator.probabilityOfStart - ) { + if (start < this.configuration.probabilityOfStart) { this.connectorsStatus.get(connectorId).skippedConsecutiveTransactions = 0; // Start transaction const startResponse = await this.startTransaction(connectorId); @@ -163,10 +173,8 @@ export default class AutomaticTransactionGenerator { } else { // Wait until end of transaction const waitTrxEnd = - Utils.getRandomInteger( - this.chargingStation.stationInfo.AutomaticTransactionGenerator.maxDuration, - this.chargingStation.stationInfo.AutomaticTransactionGenerator.minDuration - ) * 1000; + Utils.getRandomInteger(this.configuration.maxDuration, this.configuration.minDuration) * + 1000; logger.info( this.logPrefix(connectorId) + ' transaction ' + @@ -257,7 +265,7 @@ export default class AutomaticTransactionGenerator { this.connectorsStatus.get(connectorId).startDate = new Date(); this.connectorsStatus.get(connectorId).stopDate = new Date( this.connectorsStatus.get(connectorId).startDate.getTime() + - (this.chargingStation.stationInfo?.AutomaticTransactionGenerator?.stopAfterHours ?? + (this.configuration.stopAfterHours ?? Constants.CHARGING_STATION_ATG_DEFAULT_STOP_AFTER_HOURS) * 3600 * 1000 - @@ -376,9 +384,7 @@ export default class AutomaticTransactionGenerator { } private getRequireAuthorize(): boolean { - return ( - this.chargingStation.stationInfo?.AutomaticTransactionGenerator?.requireAuthorize ?? true - ); + return this.configuration?.requireAuthorize ?? true; } private logPrefix(connectorId?: number): string { diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index f283c50d..96d889ee 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -44,6 +44,7 @@ import { WSError, WebSocketCloseEventStatusCode } from '../types/WebSocket'; import WebSocket, { Data, OPEN, RawData } from 'ws'; import AutomaticTransactionGenerator from './AutomaticTransactionGenerator'; +import { AutomaticTransactionGeneratorConfiguration } from '../types/AutomaticTransactionGenerator'; import BaseError from '../exception/BaseError'; import { ChargePointErrorCode } from '../types/ocpp/ChargePointErrorCode'; import { ChargePointStatus } from '../types/ocpp/ChargePointStatus'; @@ -91,16 +92,17 @@ export default class ChargingStation { public heartbeatSetInterval!: NodeJS.Timeout; public ocppRequestService!: OCPPRequestService; public bootNotificationResponse!: BootNotificationResponse | null; + public powerDivider!: number; private readonly index: number; private configurationFile!: string; private bootNotificationRequest!: BootNotificationRequest; private connectorsConfigurationHash!: string; private ocppIncomingRequestService!: OCPPIncomingRequestService; private readonly messageBuffer: Set; - private wsConfiguredConnectionUrl!: URL; + private configuredSupervisionUrl!: URL; private wsConnectionRestarted: boolean; - private stopped: boolean; private autoReconnectRetryCount: number; + private stopped: boolean; private automaticTransactionGenerator!: AutomaticTransactionGenerator; private webSocketPingSetInterval!: NodeJS.Timeout; @@ -114,20 +116,19 @@ export default class ChargingStation { this.requests = new Map(); this.messageBuffer = new Set(); this.initialize(); - this.authorizedTags = this.getAuthorizedTags(); } private get wsConnectionUrl(): URL { - return this.getSupervisionUrlOcppConfiguration() - ? new URL( - ChargingStationConfigurationUtils.getConfigurationKey( + return new URL( + (this.getSupervisionUrlOcppConfiguration() + ? ChargingStationConfigurationUtils.getConfigurationKey( this, this.getSupervisionUrlOcppKey() - ).value + - '/' + - this.stationInfo.chargingStationId - ) - : this.wsConfiguredConnectionUrl; + ).value + : this.configuredSupervisionUrl.href) + + '/' + + this.stationInfo.chargingStationId + ); } public logPrefix(): string { @@ -162,11 +163,12 @@ export default class ChargingStation { return this.stationInfo.mayAuthorizeAtRemoteStart ?? true; } - public getNumberOfPhases(): number | undefined { - switch (this.getCurrentOutType()) { + public getNumberOfPhases(stationInfo?: ChargingStationInfo): number | undefined { + const localStationInfo: ChargingStationInfo = stationInfo ?? this.stationInfo; + switch (this.getCurrentOutType(stationInfo)) { case CurrentType.AC: - return !Utils.isUndefined(this.stationInfo.numberOfPhases) - ? this.stationInfo.numberOfPhases + return !Utils.isUndefined(localStationInfo.numberOfPhases) + ? localStationInfo.numberOfPhases : 3; case CurrentType.DC: return 0; @@ -217,22 +219,23 @@ export default class ChargingStation { return this.connectors.get(id); } - public getCurrentOutType(): CurrentType | undefined { - return this.stationInfo.currentOutType ?? CurrentType.AC; + public getCurrentOutType(stationInfo?: ChargingStationInfo): CurrentType { + return (stationInfo ?? this.stationInfo).currentOutType ?? CurrentType.AC; } public getOcppStrictCompliance(): boolean { return this.stationInfo?.ocppStrictCompliance ?? false; } - public getVoltageOut(): number | undefined { + public getVoltageOut(stationInfo?: ChargingStationInfo): number | undefined { const defaultVoltageOut = ChargingStationUtils.getDefaultVoltageOut( - this.getCurrentOutType(), + this.getCurrentOutType(stationInfo), this.templateFile, this.logPrefix() ); - return !Utils.isUndefined(this.stationInfo.voltageOut) - ? this.stationInfo.voltageOut + const localStationInfo: ChargingStationInfo = stationInfo ?? this.stationInfo; + return !Utils.isUndefined(localStationInfo.voltageOut) + ? localStationInfo.voltageOut : defaultVoltageOut; } @@ -250,9 +253,9 @@ export default class ChargingStation { this.getAmperageLimitation() * this.getNumberOfConnectors() ) : DCElectricUtils.power(this.getVoltageOut(), this.getAmperageLimitation())) / - this.stationInfo.powerDivider; + this.powerDivider; } - const connectorMaximumPower = this.getMaximumPower() / this.stationInfo.powerDivider; + const connectorMaximumPower = this.getMaximumPower() / this.powerDivider; const connectorChargingProfilePowerLimit = this.getChargingProfilePowerLimit(connectorId); return Math.min( isNaN(connectorMaximumPower) ? Infinity : connectorMaximumPower, @@ -487,7 +490,7 @@ export default class ChargingStation { FileUtils.watchJsonFile( this.logPrefix(), FileType.Authorization, - this.getAuthorizationFile(), + ChargingStationUtils.getAuthorizationFile(this.stationInfo), this.authorizedTags ); // Monitor charging station template file @@ -507,12 +510,7 @@ export default class ChargingStation { // Initialize this.initialize(); // Restart the ATG - if ( - !this.stationInfo.AutomaticTransactionGenerator.enable && - this.automaticTransactionGenerator - ) { - this.automaticTransactionGenerator.stop(); - } + this.stopAutomaticTransactionGenerator(); this.startAutomaticTransactionGenerator(); if (this.getEnableStatistics()) { this.performanceStatistics.restart(); @@ -568,8 +566,7 @@ export default class ChargingStation { public async reset(reason?: StopTransactionReason): Promise { await this.stop(reason); await Utils.sleep(this.stationInfo.resetTime); - this.stationInfo = this.getStationInfo(); - this.stationInfo?.Connectors && delete this.stationInfo.Connectors; + this.initialize(); this.start(); } @@ -616,7 +613,7 @@ export default class ChargingStation { : DCElectricUtils.power(this.getVoltageOut(), limit); } - const connectorMaximumPower = this.getMaximumPower() / this.stationInfo.powerDivider; + const connectorMaximumPower = this.getMaximumPower() / this.powerDivider; if (limit > connectorMaximumPower) { logger.error( `${this.logPrefix()} Charging profile id ${ @@ -728,74 +725,99 @@ export default class ChargingStation { } private getStationInfoFromTemplate(): ChargingStationInfo { - const stationInfo: ChargingStationInfo = this.getTemplateFromFile(); - if (Utils.isNullOrUndefined(stationInfo)) { + const stationTemplate: ChargingStationTemplate = this.getTemplateFromFile(); + if (Utils.isNullOrUndefined(stationTemplate)) { const errorMsg = 'Failed to read charging station template file'; logger.error(`${this.logPrefix()} ${errorMsg}`); throw new BaseError(errorMsg); } - if (Utils.isEmptyObject(stationInfo)) { + if (Utils.isEmptyObject(stationTemplate)) { const errorMsg = `Empty charging station information from template file ${this.templateFile}`; logger.error(`${this.logPrefix()} ${errorMsg}`); throw new BaseError(errorMsg); } - const chargingStationId = ChargingStationUtils.getChargingStationId(this.index, stationInfo); // Deprecation template keys section ChargingStationUtils.warnDeprecatedTemplateKey( - stationInfo, + stationTemplate, 'supervisionUrl', this.templateFile, this.logPrefix(), "Use 'supervisionUrls' instead" ); ChargingStationUtils.convertDeprecatedTemplateKey( - stationInfo, + stationTemplate, 'supervisionUrl', 'supervisionUrls' ); - if (!Utils.isEmptyArray(stationInfo.power)) { - stationInfo.power = stationInfo.power as number[]; - const powerArrayRandomIndex = Math.floor(Utils.secureRandom() * stationInfo.power.length); + const stationInfo: ChargingStationInfo = + ChargingStationUtils.stationTemplateToStationInfo(stationTemplate); + stationInfo.chargingStationId = ChargingStationUtils.getChargingStationId( + this.index, + stationTemplate + ); + ChargingStationUtils.createSerialNumber(stationTemplate, stationInfo); + if (!Utils.isEmptyArray(stationTemplate.power)) { + stationTemplate.power = stationTemplate.power as number[]; + const powerArrayRandomIndex = Math.floor(Utils.secureRandom() * stationTemplate.power.length); stationInfo.maximumPower = - stationInfo.powerUnit === PowerUnits.KILO_WATT - ? stationInfo.power[powerArrayRandomIndex] * 1000 - : stationInfo.power[powerArrayRandomIndex]; + stationTemplate.powerUnit === PowerUnits.KILO_WATT + ? stationTemplate.power[powerArrayRandomIndex] * 1000 + : stationTemplate.power[powerArrayRandomIndex]; } else { - stationInfo.power = stationInfo.power as number; + stationTemplate.power = stationTemplate.power as number; stationInfo.maximumPower = - stationInfo.powerUnit === PowerUnits.KILO_WATT - ? stationInfo.power * 1000 - : stationInfo.power; - } - delete stationInfo.power; - delete stationInfo.powerUnit; - stationInfo.chargingStationId = chargingStationId; - stationInfo.resetTime = stationInfo.resetTime - ? stationInfo.resetTime * 1000 + stationTemplate.powerUnit === PowerUnits.KILO_WATT + ? stationTemplate.power * 1000 + : stationTemplate.power; + } + stationInfo.resetTime = stationTemplate.resetTime + ? stationTemplate.resetTime * 1000 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME; + const configuredMaxConnectors = ChargingStationUtils.getConfiguredNumberOfConnectors( + this.index, + stationTemplate + ); + ChargingStationUtils.checkConfiguredMaxConnectors( + configuredMaxConnectors, + this.templateFile, + Utils.logPrefix() + ); + const templateMaxConnectors = + ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate); + ChargingStationUtils.checkTemplateMaxConnectors( + templateMaxConnectors, + this.templateFile, + Utils.logPrefix() + ); + if ( + configuredMaxConnectors > + (stationTemplate?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) && + !stationTemplate?.randomConnectors + ) { + logger.warn( + `${this.logPrefix()} Number of connectors exceeds the number of connector configurations in template ${ + this.templateFile + }, forcing random connector configurations affectation` + ); + stationInfo.randomConnectors = true; + } + // Build connectors if needed (FIXME: should be factored out) + this.initializeConnectors(stationInfo, configuredMaxConnectors, templateMaxConnectors); + stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo); + ChargingStationUtils.createStationInfoHash(stationInfo); return stationInfo; } private getStationInfoFromFile(): ChargingStationInfo | null { let stationInfo: ChargingStationInfo = null; - if (this.getStationInfoPersistentConfiguration()) { - stationInfo = this.getConfigurationFromFile()?.stationInfo ?? null; - } - if (stationInfo) { - stationInfo = ChargingStationUtils.createStationInfoHash(stationInfo); - } + this.getStationInfoPersistentConfiguration() && + (stationInfo = this.getConfigurationFromFile()?.stationInfo ?? null); + stationInfo && ChargingStationUtils.createStationInfoHash(stationInfo); return stationInfo; } private getStationInfo(): ChargingStationInfo { const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate(); - this.hashId = ChargingStationUtils.getHashId(stationInfoFromTemplate); - this.configurationFile = path.join( - path.resolve(__dirname, '../'), - 'assets', - 'configurations', - this.hashId + '.json' - ); const stationInfoFromFile: ChargingStationInfo = this.getStationInfoFromFile(); // Priority: charging station info from template > charging station info from configuration file > charging station info attribute if (stationInfoFromFile?.templateHash === stationInfoFromTemplate.templateHash) { @@ -804,7 +826,7 @@ export default class ChargingStation { } return stationInfoFromFile; } - ChargingStationUtils.createSerialNumber(stationInfoFromTemplate, stationInfoFromFile); + ChargingStationUtils.createSerialNumber(this.getTemplateFromFile(), stationInfoFromFile); return stationInfoFromTemplate; } @@ -835,50 +857,38 @@ export default class ChargingStation { } private initialize(): void { - this.stationInfo = this.getStationInfo(); + this.hashId = ChargingStationUtils.getHashId(this.index, this.getTemplateFromFile()); logger.info(`${this.logPrefix()} Charging station hashId '${this.hashId}'`); - this.bootNotificationRequest = ChargingStationUtils.createBootNotificationRequest( - this.stationInfo - ); - this.ocppConfiguration = this.getOcppConfiguration(); - this.stationInfo?.Configuration && delete this.stationInfo.Configuration; - this.wsConfiguredConnectionUrl = new URL( - this.getConfiguredSupervisionUrl().href + '/' + this.stationInfo.chargingStationId + this.configurationFile = path.join( + path.resolve(__dirname, '../'), + 'assets', + 'configurations', + this.hashId + '.json' ); - // Build connectors if needed - const maxConnectors = this.getMaxNumberOfConnectors(); - this.checkMaxConnectors(maxConnectors); - const templateMaxConnectors = this.getTemplateMaxNumberOfConnectors(); - this.checkTemplateMaxConnectors(templateMaxConnectors); - if ( - maxConnectors > - (this.stationInfo?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) && - !this.stationInfo?.randomConnectors - ) { - logger.warn( - `${this.logPrefix()} Number of connectors exceeds the number of connector configurations in template ${ - this.templateFile - }, forcing random connector configurations affectation` - ); - this.stationInfo.randomConnectors = true; - } - this.initializeConnectors(this.stationInfo, maxConnectors, templateMaxConnectors); - this.stationInfo.maximumAmperage = this.getMaximumAmperage(); - if (this.stationInfo) { - this.stationInfo = ChargingStationUtils.createStationInfoHash(this.stationInfo); - } + this.stationInfo = this.getStationInfo(); this.saveStationInfo(); // Avoid duplication of connectors related information in RAM this.stationInfo?.Connectors && delete this.stationInfo.Connectors; - // OCPP configuration - this.initializeOcppConfiguration(); + this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl(); if (this.getEnableStatistics()) { this.performanceStatistics = PerformanceStatistics.getInstance( this.hashId, this.stationInfo.chargingStationId, - this.wsConnectionUrl + this.configuredSupervisionUrl ); } + this.bootNotificationRequest = ChargingStationUtils.createBootNotificationRequest( + this.stationInfo + ); + this.authorizedTags = ChargingStationUtils.getAuthorizedTags( + this.stationInfo, + this.templateFile, + this.logPrefix() + ); + this.powerDivider = this.getPowerDivider(); + // OCPP configuration + this.ocppConfiguration = this.getOcppConfiguration(); + this.initializeOcppConfiguration(); switch (this.getOcppVersion()) { case OCPPVersion.VERSION_16: this.ocppIncomingRequestService = @@ -898,7 +908,6 @@ export default class ChargingStation { status: RegistrationStatus.ACCEPTED, }; } - this.stationInfo.powerDivider = this.getPowerDivider(); } private initializeOcppConfiguration(): void { @@ -934,7 +943,7 @@ export default class ChargingStation { ChargingStationConfigurationUtils.addConfigurationKey( this, this.getSupervisionUrlOcppKey(), - this.getConfiguredSupervisionUrl().href, + this.configuredSupervisionUrl.href, { reboot: true } ); } else if ( @@ -1065,7 +1074,7 @@ export default class ChargingStation { private initializeConnectors( stationInfo: ChargingStationInfo, - maxConnectors: number, + configuredMaxConnectors: number, templateMaxConnectors: number ): void { if (!stationInfo?.Connectors && this.connectors.size === 0) { @@ -1085,7 +1094,7 @@ export default class ChargingStation { if (stationInfo?.Connectors) { const connectorsConfigHash = crypto .createHash(Constants.DEFAULT_HASH_ALGORITHM) - .update(JSON.stringify(stationInfo?.Connectors) + maxConnectors.toString()) + .update(JSON.stringify(stationInfo?.Connectors) + configuredMaxConnectors.toString()) .digest('hex'); const connectorsConfigChanged = this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash; @@ -1098,7 +1107,7 @@ export default class ChargingStation { const lastConnectorId = Utils.convertToInt(lastConnector); if ( lastConnectorId === 0 && - this.getUseConnectorId0() && + this.getUseConnectorId0(stationInfo) && stationInfo?.Connectors[lastConnector] ) { this.connectors.set( @@ -1113,7 +1122,7 @@ export default class ChargingStation { } // Generate all connectors if ((stationInfo?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0) { - for (let index = 1; index <= maxConnectors; index++) { + for (let index = 1; index <= configuredMaxConnectors; index++) { const randConnectorId = stationInfo?.randomConnectors ? Utils.getRandomInteger(Utils.convertToInt(lastConnector), 1) : index; @@ -1143,32 +1152,6 @@ export default class ChargingStation { } } - private checkMaxConnectors(maxConnectors: number): void { - if (maxConnectors <= 0) { - logger.warn( - `${this.logPrefix()} Charging station information from template ${ - this.templateFile - } with ${maxConnectors} connectors` - ); - } - } - - private checkTemplateMaxConnectors(templateMaxConnectors: number): void { - if (templateMaxConnectors === 0) { - logger.warn( - `${this.logPrefix()} Charging station information from template ${ - this.templateFile - } with empty connectors configuration` - ); - } else if (templateMaxConnectors < 0) { - logger.error( - `${this.logPrefix()} Charging station information from template ${ - this.templateFile - } with no connectors configuration defined` - ); - } - } - private getConfigurationFromFile(): ChargingStationConfiguration | null { let configuration: ChargingStationConfiguration = null; if (this.configurationFile && fs.existsSync(this.configurationFile)) { @@ -1494,43 +1477,10 @@ export default class ChargingStation { logger.error(this.logPrefix() + ' WebSocket error: %j', error); } - private getAuthorizationFile(): string | undefined { - return ( - this.stationInfo.authorizationFile && - path.join( - path.resolve(__dirname, '../'), - 'assets', - path.basename(this.stationInfo.authorizationFile) - ) - ); - } - - private getAuthorizedTags(): string[] { - let authorizedTags: string[] = []; - const authorizationFile = this.getAuthorizationFile(); - if (authorizationFile) { - try { - // Load authorization file - authorizedTags = JSON.parse(fs.readFileSync(authorizationFile, 'utf8')) as string[]; - } catch (error) { - FileUtils.handleFileException( - this.logPrefix(), - FileType.Authorization, - authorizationFile, - error as NodeJS.ErrnoException - ); - } - } else { - logger.info( - this.logPrefix() + ' No authorization file given in template file ' + this.templateFile - ); - } - return authorizedTags; - } - - private getUseConnectorId0(): boolean | undefined { - return !Utils.isUndefined(this.stationInfo.useConnectorId0) - ? this.stationInfo.useConnectorId0 + private getUseConnectorId0(stationInfo?: ChargingStationInfo): boolean | undefined { + const localStationInfo = stationInfo ?? this.stationInfo; + return !Utils.isUndefined(localStationInfo.useConnectorId0) + ? localStationInfo.useConnectorId0 : true; } @@ -1585,50 +1535,28 @@ export default class ChargingStation { private getPowerDivider(): number { let powerDivider = this.getNumberOfConnectors(); - if (this.stationInfo.powerSharedByConnectors) { + if (this.stationInfo?.powerSharedByConnectors) { powerDivider = this.getNumberOfRunningTransactions(); } return powerDivider; } - private getTemplateMaxNumberOfConnectors(): number { - if (!this.stationInfo?.Connectors) { - return -1; - } - return Object.keys(this.stationInfo?.Connectors).length; - } - - private getMaxNumberOfConnectors(): number { - let maxConnectors: number; - if (!Utils.isEmptyArray(this.stationInfo.numberOfConnectors)) { - const numberOfConnectors = this.stationInfo.numberOfConnectors as number[]; - // Distribute evenly the number of connectors - maxConnectors = numberOfConnectors[(this.index - 1) % numberOfConnectors.length]; - } else if (!Utils.isUndefined(this.stationInfo.numberOfConnectors)) { - maxConnectors = this.stationInfo.numberOfConnectors as number; - } else { - maxConnectors = this.stationInfo?.Connectors[0] - ? this.getTemplateMaxNumberOfConnectors() - 1 - : this.getTemplateMaxNumberOfConnectors(); - } - return maxConnectors; - } - - private getMaximumPower(): number { - return (this.stationInfo['maxPower'] as number) ?? this.stationInfo.maximumPower; + private getMaximumPower(stationInfo?: ChargingStationInfo): number { + const localStationInfo = stationInfo ?? this.stationInfo; + return (localStationInfo['maxPower'] as number) ?? localStationInfo.maximumPower; } - private getMaximumAmperage(): number | undefined { - const maximumPower = this.getMaximumPower(); - switch (this.getCurrentOutType()) { + private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined { + const maximumPower = this.getMaximumPower(stationInfo); + switch (this.getCurrentOutType(stationInfo)) { case CurrentType.AC: return ACElectricUtils.amperagePerPhaseFromPower( - this.getNumberOfPhases(), + this.getNumberOfPhases(stationInfo), maximumPower / this.getNumberOfConnectors(), - this.getVoltageOut() + this.getVoltageOut(stationInfo) ); case CurrentType.DC: - return DCElectricUtils.amperage(maximumPower, this.getVoltageOut()); + return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo)); } } @@ -1741,9 +1669,12 @@ export default class ChargingStation { } private startAutomaticTransactionGenerator() { - if (this.stationInfo.AutomaticTransactionGenerator.enable) { + if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable) { if (!this.automaticTransactionGenerator) { - this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this); + this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance( + this.getAutomaticTransactionGeneratorConfigurationFromTemplate(), + this + ); } if (!this.automaticTransactionGenerator.started) { this.automaticTransactionGenerator.start(); @@ -1751,6 +1682,13 @@ export default class ChargingStation { } } + private stopAutomaticTransactionGenerator(): void { + if (this.automaticTransactionGenerator?.started) { + this.automaticTransactionGenerator.stop(); + this.automaticTransactionGenerator = null; + } + } + private async stopMessageSequence( reason: StopTransactionReason = StopTransactionReason.NONE ): Promise { @@ -1758,12 +1696,9 @@ export default class ChargingStation { this.stopWebSocketPing(); // Stop heartbeat this.stopHeartbeat(); - // Stop the ATG - if ( - this.stationInfo.AutomaticTransactionGenerator.enable && - this.automaticTransactionGenerator?.started - ) { - this.automaticTransactionGenerator.stop(); + // Stop ongoing transactions + if (this.automaticTransactionGenerator?.configuration?.enable) { + this.stopAutomaticTransactionGenerator(); } else { for (const connectorId of this.connectors.keys()) { if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted) { @@ -1966,12 +1901,8 @@ export default class ChargingStation { // Stop heartbeat this.stopHeartbeat(); // Stop the ATG if needed - if ( - this.stationInfo.AutomaticTransactionGenerator.enable && - this.stationInfo.AutomaticTransactionGenerator.stopOnConnectionFailure && - this.automaticTransactionGenerator?.started - ) { - this.automaticTransactionGenerator.stop(); + if (this.automaticTransactionGenerator?.configuration?.stopOnConnectionFailure) { + this.stopAutomaticTransactionGenerator(); } if ( this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() || @@ -2008,6 +1939,10 @@ export default class ChargingStation { } } + private getAutomaticTransactionGeneratorConfigurationFromTemplate(): AutomaticTransactionGeneratorConfiguration | null { + return this.getTemplateFromFile()?.AutomaticTransactionGenerator ?? null; + } + private initializeConnectorStatus(connectorId: number): void { this.getConnectorStatus(connectorId).idTagLocalAuthorized = false; this.getConnectorStatus(connectorId).idTagAuthorized = false; diff --git a/src/charging-station/ChargingStationUtils.ts b/src/charging-station/ChargingStationUtils.ts index afe9a52f..b57e98e6 100644 --- a/src/charging-station/ChargingStationUtils.ts +++ b/src/charging-station/ChargingStationUtils.ts @@ -13,14 +13,18 @@ import { ChargingStationConfigurationUtils } from './ChargingStationConfiguratio import ChargingStationInfo from '../types/ChargingStationInfo'; import Configuration from '../utils/Configuration'; import Constants from '../utils/Constants'; +import { FileType } from '../types/FileType'; +import FileUtils from '../utils/FileUtils'; import { SampledValueTemplate } from '../types/MeasurandPerPhaseSampledValueTemplates'; import { StandardParametersKey } from '../types/ocpp/Configuration'; import Utils from '../utils/Utils'; import { WebSocketCloseEventStatusString } from '../types/WebSocket'; import { WorkerProcessType } from '../types/Worker'; import crypto from 'crypto'; +import fs from 'fs'; import logger from '../utils/Logger'; import moment from 'moment'; +import path from 'path'; export class ChargingStationUtils { public static getChargingStationId( @@ -40,34 +44,92 @@ export class ChargingStationUtils { idSuffix; } - public static getHashId(stationInfo: ChargingStationInfo): string { + public static getHashId(index: number, stationTemplate: ChargingStationTemplate): string { const hashBootNotificationRequest = { - chargePointModel: stationInfo.chargePointModel, - chargePointVendor: stationInfo.chargePointVendor, - ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumberPrefix) && { - chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumberPrefix, + chargePointModel: stationTemplate.chargePointModel, + chargePointVendor: stationTemplate.chargePointVendor, + ...(!Utils.isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && { + chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix, }), - ...(!Utils.isUndefined(stationInfo.chargePointSerialNumberPrefix) && { - chargePointSerialNumber: stationInfo.chargePointSerialNumberPrefix, + ...(!Utils.isUndefined(stationTemplate.chargePointSerialNumberPrefix) && { + chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix, }), - ...(!Utils.isUndefined(stationInfo.firmwareVersion) && { - firmwareVersion: stationInfo.firmwareVersion, + ...(!Utils.isUndefined(stationTemplate.firmwareVersion) && { + firmwareVersion: stationTemplate.firmwareVersion, }), - ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }), - ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }), - ...(!Utils.isUndefined(stationInfo.meterSerialNumberPrefix) && { - meterSerialNumber: stationInfo.meterSerialNumberPrefix, + ...(!Utils.isUndefined(stationTemplate.iccid) && { iccid: stationTemplate.iccid }), + ...(!Utils.isUndefined(stationTemplate.imsi) && { imsi: stationTemplate.imsi }), + ...(!Utils.isUndefined(stationTemplate.meterSerialNumberPrefix) && { + meterSerialNumber: stationTemplate.meterSerialNumberPrefix, }), - ...(!Utils.isUndefined(stationInfo.meterType) && { - meterType: stationInfo.meterType, + ...(!Utils.isUndefined(stationTemplate.meterType) && { + meterType: stationTemplate.meterType, }), }; return crypto .createHash(Constants.DEFAULT_HASH_ALGORITHM) - .update(JSON.stringify(hashBootNotificationRequest) + stationInfo.chargingStationId) + .update( + JSON.stringify(hashBootNotificationRequest) + + ChargingStationUtils.getChargingStationId(index, stationTemplate) + ) .digest('hex'); } + public static getTemplateMaxNumberOfConnectors(stationTemplate: ChargingStationTemplate): number { + const templateConnectors = stationTemplate?.Connectors; + if (!templateConnectors) { + return -1; + } + return Object.keys(templateConnectors).length; + } + + public static checkTemplateMaxConnectors( + templateMaxConnectors: number, + templateFile: string, + logPrefix: string + ): void { + if (templateMaxConnectors === 0) { + logger.warn( + `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration` + ); + } else if (templateMaxConnectors < 0) { + logger.error( + `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined` + ); + } + } + + public static getConfiguredNumberOfConnectors( + index: number, + stationTemplate: ChargingStationTemplate + ): number { + let configuredMaxConnectors: number; + if (!Utils.isEmptyArray(stationTemplate.numberOfConnectors)) { + const numberOfConnectors = stationTemplate.numberOfConnectors as number[]; + // Distribute evenly the number of connectors + configuredMaxConnectors = numberOfConnectors[(index - 1) % numberOfConnectors.length]; + } else if (!Utils.isUndefined(stationTemplate.numberOfConnectors)) { + configuredMaxConnectors = stationTemplate.numberOfConnectors as number; + } else { + configuredMaxConnectors = stationTemplate?.Connectors[0] + ? ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate) - 1 + : ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate); + } + return configuredMaxConnectors; + } + + public static checkConfiguredMaxConnectors( + configuredMaxConnectors: number, + templateFile: string, + logPrefix: string + ): void { + if (configuredMaxConnectors <= 0) { + logger.warn( + `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors` + ); + } + } + public static createBootNotificationRequest( stationInfo: ChargingStationInfo ): BootNotificationRequest { @@ -157,7 +219,20 @@ export class ChargingStationUtils { } } - public static createStationInfoHash(stationInfo: ChargingStationInfo): ChargingStationInfo { + public static stationTemplateToStationInfo( + stationTemplate: ChargingStationTemplate + ): ChargingStationInfo { + stationTemplate = Utils.cloneObject(stationTemplate); + delete stationTemplate.power; + delete stationTemplate.powerUnit; + delete stationTemplate.Configuration; + delete stationTemplate.AutomaticTransactionGenerator; + delete stationTemplate.chargeBoxSerialNumberPrefix; + delete stationTemplate.chargePointSerialNumberPrefix; + return stationTemplate; + } + + public static createStationInfoHash(stationInfo: ChargingStationInfo): void { const previousInfoHash = stationInfo?.infoHash ?? ''; delete stationInfo.infoHash; const currentInfoHash = crypto @@ -172,13 +247,15 @@ export class ChargingStationUtils { } else { stationInfo.infoHash = previousInfoHash; } - return stationInfo; } public static createSerialNumber( + stationTemplate: ChargingStationTemplate, stationInfo: ChargingStationInfo, - existingStationInfo?: ChargingStationInfo | null, - params: { randomSerialNumberUpperCase?: boolean; randomSerialNumber?: boolean } = { + params: { + randomSerialNumberUpperCase?: boolean; + randomSerialNumber?: boolean; + } = { randomSerialNumberUpperCase: true, randomSerialNumber: true, } @@ -186,29 +263,29 @@ export class ChargingStationUtils { params = params ?? {}; params.randomSerialNumberUpperCase = params?.randomSerialNumberUpperCase ?? true; params.randomSerialNumber = params?.randomSerialNumber ?? true; - if (existingStationInfo) { - existingStationInfo?.chargePointSerialNumber && - (stationInfo.chargePointSerialNumber = existingStationInfo.chargePointSerialNumber); - existingStationInfo?.chargeBoxSerialNumber && - (stationInfo.chargeBoxSerialNumber = existingStationInfo.chargeBoxSerialNumber); - existingStationInfo?.meterSerialNumber && - (stationInfo.meterSerialNumber = existingStationInfo.meterSerialNumber); - } else { - const serialNumberSuffix = params?.randomSerialNumber - ? ChargingStationUtils.getRandomSerialNumberSuffix({ - upperCase: params.randomSerialNumberUpperCase, - }) - : ''; - stationInfo.chargePointSerialNumber = - stationInfo?.chargePointSerialNumberPrefix && - stationInfo.chargePointSerialNumberPrefix + serialNumberSuffix; - stationInfo.chargeBoxSerialNumber = - stationInfo?.chargeBoxSerialNumberPrefix && - stationInfo.chargeBoxSerialNumberPrefix + serialNumberSuffix; - stationInfo.meterSerialNumber = - stationInfo?.meterSerialNumberPrefix && - stationInfo.meterSerialNumberPrefix + serialNumberSuffix; - } + const serialNumberSuffix = params?.randomSerialNumber + ? ChargingStationUtils.getRandomSerialNumberSuffix({ + upperCase: params.randomSerialNumberUpperCase, + }) + : ''; + stationTemplate?.chargePointSerialNumberPrefix && + stationInfo && + Utils.isNullOrUndefined(stationInfo?.chargePointSerialNumber) + ? (stationInfo.chargePointSerialNumber = + stationTemplate.chargePointSerialNumberPrefix + serialNumberSuffix) + : stationInfo && delete stationInfo.chargePointSerialNumber; + stationTemplate?.chargeBoxSerialNumberPrefix && + stationInfo && + Utils.isNullOrUndefined(stationInfo?.chargeBoxSerialNumber) + ? (stationInfo.chargeBoxSerialNumber = + stationTemplate.chargeBoxSerialNumberPrefix + serialNumberSuffix) + : stationInfo && delete stationInfo.chargeBoxSerialNumber; + stationTemplate?.meterSerialNumberPrefix && + stationInfo && + Utils.isNullOrUndefined(stationInfo?.meterSerialNumber) + ? (stationInfo.meterSerialNumber = + stationTemplate.meterSerialNumberPrefix + serialNumberSuffix) + : stationInfo && delete stationInfo.meterSerialNumber; } public static getAmperageLimitationUnitDivider(stationInfo: ChargingStationInfo): number { @@ -434,6 +511,42 @@ export class ChargingStationUtils { ); } + public static getAuthorizedTags( + stationInfo: ChargingStationInfo, + templateFile: string, + logPrefix: string + ): string[] { + let authorizedTags: string[] = []; + const authorizationFile = ChargingStationUtils.getAuthorizationFile(stationInfo); + if (authorizationFile) { + try { + // Load authorization file + authorizedTags = JSON.parse(fs.readFileSync(authorizationFile, 'utf8')) as string[]; + } catch (error) { + FileUtils.handleFileException( + logPrefix, + FileType.Authorization, + authorizationFile, + error as NodeJS.ErrnoException + ); + } + } else { + logger.info(logPrefix + ' No authorization file given in template file ' + templateFile); + } + return authorizedTags; + } + + public static getAuthorizationFile(stationInfo: ChargingStationInfo): string | undefined { + return ( + stationInfo.authorizationFile && + path.join( + path.resolve(__dirname, '../'), + 'assets', + path.basename(stationInfo.authorizationFile) + ) + ); + } + private static getRandomSerialNumberSuffix(params?: { randomBytesLength?: number; upperCase?: boolean; diff --git a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts index 1bcce705..90c04035 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts @@ -335,7 +335,7 @@ export default class OCPP16ResponseService extends OCPPResponseService { requestPayload.idTag ); if (chargingStation.stationInfo.powerSharedByConnectors) { - chargingStation.stationInfo.powerDivider++; + chargingStation.powerDivider++; } const configuredMeterValueSampleInterval = ChargingStationConfigurationUtils.getConfigurationKey( @@ -441,7 +441,7 @@ export default class OCPP16ResponseService extends OCPPResponseService { OCPP16ChargePointStatus.AVAILABLE; } if (chargingStation.stationInfo.powerSharedByConnectors) { - chargingStation.stationInfo.powerDivider--; + chargingStation.powerDivider--; } logger.info( chargingStation.logPrefix() + diff --git a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts index 1c8e1369..0599cfa0 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts @@ -770,16 +770,16 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { chargingStation: ChargingStation, measurandType: OCPP16MeterValueMeasurand ): void { - if (Utils.isUndefined(chargingStation.stationInfo.powerDivider)) { + if (Utils.isUndefined(chargingStation.powerDivider)) { const errMsg = `${chargingStation.logPrefix()} MeterValues measurand ${ measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER }: powerDivider is undefined`; logger.error(errMsg); throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES); - } else if (chargingStation.stationInfo?.powerDivider <= 0) { + } else if (chargingStation?.powerDivider <= 0) { const errMsg = `${chargingStation.logPrefix()} MeterValues measurand ${ measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER - }: powerDivider have zero or below value ${chargingStation.stationInfo.powerDivider}`; + }: powerDivider have zero or below value ${chargingStation.powerDivider}`; logger.error(errMsg); throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES); } diff --git a/src/types/AutomaticTransactionGenerator.ts b/src/types/AutomaticTransactionGenerator.ts index 940a5d75..83e88c68 100644 --- a/src/types/AutomaticTransactionGenerator.ts +++ b/src/types/AutomaticTransactionGenerator.ts @@ -1,3 +1,15 @@ +export interface AutomaticTransactionGeneratorConfiguration { + enable: boolean; + minDuration: number; + maxDuration: number; + minDelayBetweenTwoTransactions: number; + maxDelayBetweenTwoTransactions: number; + probabilityOfStart: number; + stopAfterHours: number; + stopOnConnectionFailure: boolean; + requireAuthorize?: boolean; +} + export interface Status { start?: boolean; startDate?: Date; diff --git a/src/types/ChargingStationInfo.ts b/src/types/ChargingStationInfo.ts index 1cf27163..1c6415ae 100644 --- a/src/types/ChargingStationInfo.ts +++ b/src/types/ChargingStationInfo.ts @@ -1,13 +1,21 @@ import ChargingStationTemplate from './ChargingStationTemplate'; -export default interface ChargingStationInfo extends ChargingStationTemplate { +export default interface ChargingStationInfo + extends Omit< + ChargingStationTemplate, + | 'AutomaticTransactionGenerator' + | 'Configuration' + | 'power' + | 'powerUnit' + | 'chargeBoxSerialNumberPrefix' + | 'chargePointSerialNumberPrefix' + > { infoHash?: string; chargingStationId?: string; chargeBoxSerialNumber?: string; chargePointSerialNumber?: string; meterSerialNumber?: string; maximumPower?: number; // Always in Watt - powerDivider?: number; maximumAmperage?: number; // Always in Ampere } diff --git a/src/types/ChargingStationTemplate.ts b/src/types/ChargingStationTemplate.ts index 212cb64e..db8ff382 100644 --- a/src/types/ChargingStationTemplate.ts +++ b/src/types/ChargingStationTemplate.ts @@ -1,3 +1,4 @@ +import { AutomaticTransactionGeneratorConfiguration } from './AutomaticTransactionGenerator'; import ChargingStationOcppConfiguration from './ChargingStationOcppConfiguration'; import { ClientOptions } from 'ws'; import { ClientRequestArgs } from 'http'; @@ -29,18 +30,6 @@ export enum Voltage { VOLTAGE_800 = 800, } -export interface AutomaticTransactionGenerator { - enable: boolean; - minDuration: number; - maxDuration: number; - minDelayBetweenTwoTransactions: number; - maxDelayBetweenTwoTransactions: number; - probabilityOfStart: number; - stopAfterHours: number; - stopOnConnectionFailure: boolean; - requireAuthorize?: boolean; -} - export type WsOptions = ClientOptions & ClientRequestArgs; export default interface ChargingStationTemplate { @@ -95,6 +84,6 @@ export default interface ChargingStationTemplate { phaseLineToLineVoltageMeterValues?: boolean; customValueLimitationMeterValues?: boolean; Configuration?: ChargingStationOcppConfiguration; - AutomaticTransactionGenerator: AutomaticTransactionGenerator; + AutomaticTransactionGenerator?: AutomaticTransactionGeneratorConfiguration; Connectors: Record; } -- 2.34.1