X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;ds=sidebyside;f=src%2Fcharging-station%2FChargingStation.ts;h=3051abb7065de6382b2ffd7f2ba6d88c9bb23685;hb=98c19a0738935a04ceedd45ccd198769ef91f035;hp=434de010fef13c04d9fbe9a38977153957a5869c;hpb=9214b603daab4382ab6d1c38e91211ab38451a4c;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 434de010..3051abb7 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -1,5 +1,6 @@ // Partial Copyright Jerome Benoit. 2021. All Rights Reserved. +import { ACElectricUtils, DCElectricUtils } from '../utils/ElectricUtils'; import { AvailabilityType, BootNotificationRequest, @@ -15,11 +16,17 @@ import { RegistrationStatus, StatusNotificationResponse, } from '../types/ocpp/Responses'; +import { + ChargingProfile, + ChargingRateUnitType, + ChargingSchedulePeriod, +} from '../types/ocpp/ChargingProfile'; import ChargingStationConfiguration, { Section } from '../types/ChargingStationConfiguration'; import ChargingStationOcppConfiguration, { ConfigurationKey, } from '../types/ChargingStationOcppConfiguration'; import ChargingStationTemplate, { + AmpereUnits, CurrentType, PowerUnits, Voltage, @@ -39,7 +46,6 @@ import WebSocket, { Data, OPEN, RawData } from 'ws'; import AutomaticTransactionGenerator from './AutomaticTransactionGenerator'; import { ChargePointErrorCode } from '../types/ocpp/ChargePointErrorCode'; import { ChargePointStatus } from '../types/ocpp/ChargePointStatus'; -import { ChargingProfile } from '../types/ocpp/ChargingProfile'; import ChargingStationInfo from '../types/ChargingStationInfo'; import { ChargingStationWorkerMessageEvents } from '../types/ChargingStationWorker'; import Configuration from '../utils/Configuration'; @@ -229,6 +235,33 @@ export default class ChargingStation { : defaultVoltageOut; } + public getConnectorMaximumAvailablePower(connectorId: number): number { + let amperageLimitationPowerLimit: number; + if (this.getAmperageLimitation() < this.stationInfo.maximumAmperage) { + amperageLimitationPowerLimit = + this.getCurrentOutType() === CurrentType.AC + ? ACElectricUtils.powerTotal( + this.getNumberOfPhases(), + this.getVoltageOut(), + this.getAmperageLimitation() * this.getNumberOfConnectors() + ) + : DCElectricUtils.power(this.getVoltageOut(), this.getAmperageLimitation()); + } + const connectorMaximumPower = + ((this.stationInfo['maxPower'] as number) ?? this.stationInfo.maximumPower) / + this.stationInfo.powerDivider; + const connectorAmperageLimitationPowerLimit = + amperageLimitationPowerLimit / this.stationInfo.powerDivider; + const connectorChargingProfilePowerLimit = this.getChargingProfilePowerLimit(connectorId); + return Math.min( + isNaN(connectorMaximumPower) ? Infinity : connectorMaximumPower, + isNaN(connectorAmperageLimitationPowerLimit) + ? Infinity + : connectorAmperageLimitationPowerLimit, + isNaN(connectorChargingProfilePowerLimit) ? Infinity : connectorChargingProfilePowerLimit + ); + } + public getTransactionIdTag(transactionId: number): string | undefined { for (const connectorId of this.connectors.keys()) { if (connectorId > 0 && this.getConnectorStatus(connectorId).transactionId === transactionId) { @@ -659,8 +692,9 @@ export default class ChargingStation { ): void { const keyFound = this.getConfigurationKey(key, caseInsensitive); if (keyFound) { - const keyIndex = this.ocppConfiguration.configurationKey.indexOf(keyFound); - this.ocppConfiguration.configurationKey[keyIndex].value = value; + this.ocppConfiguration.configurationKey[ + this.ocppConfiguration.configurationKey.indexOf(keyFound) + ].value = value; this.saveOcppConfiguration(); } else { logger.error( @@ -685,6 +719,82 @@ export default class ChargingStation { } } + public getChargingProfilePowerLimit(connectorId: number): number | undefined { + const timestamp = new Date().getTime(); + let matchingChargingProfile: ChargingProfile; + let chargingSchedulePeriods: ChargingSchedulePeriod[] = []; + if (!Utils.isEmptyArray(this.getConnectorStatus(connectorId)?.chargingProfiles)) { + const chargingProfiles: ChargingProfile[] = this.getConnectorStatus( + connectorId + ).chargingProfiles.filter( + (chargingProfile) => + timestamp >= chargingProfile.chargingSchedule?.startSchedule.getTime() && + timestamp < + chargingProfile.chargingSchedule?.startSchedule.getTime() + + chargingProfile.chargingSchedule.duration * 1000 && + chargingProfile?.stackLevel === Math.max(...chargingProfiles.map((cp) => cp?.stackLevel)) + ); + if (!Utils.isEmptyArray(chargingProfiles)) { + for (const chargingProfile of chargingProfiles) { + if (!Utils.isEmptyArray(chargingProfile.chargingSchedule.chargingSchedulePeriod)) { + chargingSchedulePeriods = + chargingProfile.chargingSchedule.chargingSchedulePeriod.filter( + (chargingSchedulePeriod, index) => { + timestamp >= + chargingProfile.chargingSchedule.startSchedule.getTime() + + chargingSchedulePeriod.startPeriod * 1000 && + ((chargingProfile.chargingSchedule.chargingSchedulePeriod[index + 1] && + timestamp < + chargingProfile.chargingSchedule.startSchedule.getTime() + + chargingProfile.chargingSchedule.chargingSchedulePeriod[index + 1] + ?.startPeriod * + 1000) || + !chargingProfile.chargingSchedule.chargingSchedulePeriod[index + 1]); + } + ); + if (!Utils.isEmptyArray(chargingSchedulePeriods)) { + matchingChargingProfile = chargingProfile; + break; + } + } + } + } + } + let limit: number; + if (!Utils.isEmptyArray(chargingSchedulePeriods)) { + switch (this.getCurrentOutType()) { + case CurrentType.AC: + limit = + matchingChargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT + ? chargingSchedulePeriods[0].limit + : ACElectricUtils.powerTotal( + this.getNumberOfPhases(), + this.getVoltageOut(), + chargingSchedulePeriods[0].limit + ); + break; + case CurrentType.DC: + limit = + matchingChargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT + ? chargingSchedulePeriods[0].limit + : DCElectricUtils.power(this.getVoltageOut(), chargingSchedulePeriods[0].limit); + } + } + const connectorMaximumPower = + ((this.stationInfo['maxPower'] as number) ?? this.stationInfo.maximumPower) / + this.stationInfo.powerDivider; + if (limit > connectorMaximumPower) { + logger.error( + `${this.logPrefix()} Charging profile id ${ + matchingChargingProfile.chargingProfileId + } limit is greater than connector id ${connectorId} maximum, dump charging profiles' stack: %j`, + this.getConnectorStatus(connectorId).chargingProfiles + ); + limit = connectorMaximumPower; + } + return limit; + } + public setChargingProfile(connectorId: number, cp: ChargingProfile): void { let cpReplaced = false; if (!Utils.isEmptyArray(this.getConnectorStatus(connectorId).chargingProfiles)) { @@ -770,7 +880,10 @@ export default class ChargingStation { private getTemplateFromFile(): ChargingStationTemplate | null { let template: ChargingStationTemplate = null; try { + const measureId = `${FileType.ChargingStationTemplate} read`; + const beginId = PerformanceStatistics.beginMeasure(measureId); template = JSON.parse(fs.readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate; + PerformanceStatistics.endMeasure(measureId, beginId); } catch (error) { FileUtils.handleFileException( this.logPrefix(), @@ -784,62 +897,69 @@ export default class ChargingStation { private createSerialNumber( stationInfo: ChargingStationInfo, - params: { randomSerialNumber: boolean } = { randomSerialNumber: false } + existingStationInfo?: ChargingStationInfo, + params: { randomSerialNumberUpperCase?: boolean; randomSerialNumber?: boolean } = { + randomSerialNumberUpperCase: true, + randomSerialNumber: true, + } ): void { - const serialNumberSuffix = params.randomSerialNumber - ? this.getRandomSerialNumberSuffix({ upperCase: true }) - : ''; - stationInfo.chargePointSerialNumber = - stationInfo?.chargePointSerialNumberPrefix && - stationInfo.chargePointSerialNumberPrefix + serialNumberSuffix; - delete stationInfo.chargePointSerialNumberPrefix; - stationInfo.chargeBoxSerialNumber = - stationInfo?.chargeBoxSerialNumberPrefix && - stationInfo.chargeBoxSerialNumberPrefix + serialNumberSuffix; - delete stationInfo.chargeBoxSerialNumberPrefix; - } - - private getStationInfoFromTemplate( - params: { randomSerialNumber: boolean } = { randomSerialNumber: false } - ): ChargingStationInfo { - const templateFromFile: ChargingStationTemplate = this.getTemplateFromFile(); - const stationInfo: ChargingStationInfo = templateFromFile ?? ({} as ChargingStationInfo); + 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); + } else { + const serialNumberSuffix = params?.randomSerialNumber + ? this.getRandomSerialNumberSuffix({ upperCase: params.randomSerialNumberUpperCase }) + : ''; + stationInfo.chargePointSerialNumber = + stationInfo?.chargePointSerialNumberPrefix && + stationInfo.chargePointSerialNumberPrefix + serialNumberSuffix; + stationInfo.chargeBoxSerialNumber = + stationInfo?.chargeBoxSerialNumberPrefix && + stationInfo.chargeBoxSerialNumberPrefix + serialNumberSuffix; + } + } + + private getStationInfoFromTemplate(): ChargingStationInfo { + const stationInfo: ChargingStationInfo = + this.getTemplateFromFile() ?? ({} as ChargingStationInfo); stationInfo.hash = crypto .createHash(Constants.DEFAULT_HASH_ALGORITHM) - .update(JSON.stringify(templateFromFile)) + .update(JSON.stringify(stationInfo)) .digest('hex'); - const chargingStationId = this.getChargingStationId(templateFromFile); + const chargingStationId = this.getChargingStationId(stationInfo); // Deprecation template keys section this.warnDeprecatedTemplateKey( - templateFromFile, + stationInfo, 'supervisionUrl', chargingStationId, "Use 'supervisionUrls' instead" ); - this.convertDeprecatedTemplateKey(templateFromFile, 'supervisionUrl', 'supervisionUrls'); - this.createSerialNumber(stationInfo, params); - stationInfo.wsOptions = templateFromFile?.wsOptions ?? {}; - if (!Utils.isEmptyArray(templateFromFile.power)) { - templateFromFile.power = templateFromFile.power as number[]; - const powerArrayRandomIndex = Math.floor( - Utils.secureRandom() * templateFromFile.power.length - ); - stationInfo.maxPower = - templateFromFile.powerUnit === PowerUnits.KILO_WATT - ? templateFromFile.power[powerArrayRandomIndex] * 1000 - : templateFromFile.power[powerArrayRandomIndex]; + this.convertDeprecatedTemplateKey(stationInfo, 'supervisionUrl', 'supervisionUrls'); + stationInfo.wsOptions = stationInfo?.wsOptions ?? {}; + if (!Utils.isEmptyArray(stationInfo.power)) { + stationInfo.power = stationInfo.power as number[]; + const powerArrayRandomIndex = Math.floor(Utils.secureRandom() * stationInfo.power.length); + stationInfo.maximumPower = + stationInfo.powerUnit === PowerUnits.KILO_WATT + ? stationInfo.power[powerArrayRandomIndex] * 1000 + : stationInfo.power[powerArrayRandomIndex]; } else { - templateFromFile.power = templateFromFile.power as number; - stationInfo.maxPower = - templateFromFile.powerUnit === PowerUnits.KILO_WATT - ? templateFromFile.power * 1000 - : templateFromFile.power; + stationInfo.power = stationInfo.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 = templateFromFile.resetTime - ? templateFromFile.resetTime * 1000 + stationInfo.resetTime = stationInfo.resetTime + ? stationInfo.resetTime * 1000 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME; return stationInfo; } @@ -850,10 +970,7 @@ export default class ChargingStation { private getStationInfo(): ChargingStationInfo { const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate(); - this.hashId = this.getHashId( - this.createBootNotificationRequest(stationInfoFromTemplate), - stationInfoFromTemplate.chargingStationId - ); + this.hashId = this.getHashId(stationInfoFromTemplate); this.configurationFile = path.join( path.resolve(__dirname, '../'), 'assets', @@ -864,6 +981,7 @@ export default class ChargingStation { if (stationInfoFromFile?.hash === stationInfoFromTemplate.hash) { return stationInfoFromFile; } + this.createSerialNumber(stationInfoFromTemplate, stationInfoFromFile); return stationInfoFromTemplate; } @@ -911,13 +1029,31 @@ export default class ChargingStation { }; } - private getHashId( - bootNotificationRequest: BootNotificationRequest, - chargingStationId: string - ): string { + private getHashId(stationInfo: ChargingStationInfo): string { + const hashBootNotificationRequest = { + chargePointModel: stationInfo.chargePointModel, + chargePointVendor: stationInfo.chargePointVendor, + ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumberPrefix) && { + chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumberPrefix, + }), + ...(!Utils.isUndefined(stationInfo.chargePointSerialNumberPrefix) && { + chargePointSerialNumber: stationInfo.chargePointSerialNumberPrefix, + }), + ...(!Utils.isUndefined(stationInfo.firmwareVersion) && { + firmwareVersion: stationInfo.firmwareVersion, + }), + ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }), + ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }), + ...(!Utils.isUndefined(stationInfo.meterSerialNumber) && { + meterSerialNumber: stationInfo.meterSerialNumber, + }), + ...(!Utils.isUndefined(stationInfo.meterType) && { + meterType: stationInfo.meterType, + }), + }; return crypto .createHash(Constants.DEFAULT_HASH_ALGORITHM) - .update(JSON.stringify(bootNotificationRequest) + chargingStationId) + .update(JSON.stringify(hashBootNotificationRequest) + stationInfo.chargingStationId) .digest('hex'); } @@ -927,7 +1063,6 @@ export default class ChargingStation { this.bootNotificationRequest = this.createBootNotificationRequest(this.stationInfo); this.ocppConfiguration = this.getOcppConfiguration(); delete this.stationInfo.Configuration; - this.saveStationInfo(); // Build connectors if needed const maxConnectors = this.getMaxNumberOfConnectors(); if (maxConnectors <= 0) { @@ -1012,7 +1147,10 @@ export default class ChargingStation { } } } - // Avoid duplication of connectors related information + // The connectors attribute need to be initialized + this.stationInfo.maximumAmperage = this.getMaximumAmperage(); + this.saveStationInfo(); + // Avoid duplication of connectors related information in RAM delete this.stationInfo.Connectors; // Initialize transaction attributes on connectors for (const connectorId of this.connectors.keys()) { @@ -1071,6 +1209,15 @@ export default class ChargingStation { ) { this.deleteConfigurationKey(this.getSupervisionUrlOcppKey(), { save: false }); } + if ( + this.stationInfo.amperageLimitationOcppKey && + !this.getConfigurationKey(this.stationInfo.amperageLimitationOcppKey) + ) { + this.addConfigurationKey( + this.stationInfo.amperageLimitationOcppKey, + (this.stationInfo.maximumAmperage * this.getAmperageLimitationUnitDivider()).toString() + ); + } if (!this.getConfigurationKey(StandardParametersKey.SupportedFeatureProfiles)) { this.addConfigurationKey( StandardParametersKey.SupportedFeatureProfiles, @@ -1133,9 +1280,14 @@ export default class ChargingStation { let configuration: ChargingStationConfiguration = null; if (this.configurationFile && fs.existsSync(this.configurationFile)) { try { + const measureId = `${FileType.ChargingStationConfiguration} read`; + const beginId = PerformanceStatistics.beginMeasure( + `${FileType.ChargingStationConfiguration} read` + ); configuration = JSON.parse( fs.readFileSync(this.configurationFile, 'utf8') ) as ChargingStationConfiguration; + PerformanceStatistics.endMeasure(measureId, beginId); } catch (error) { FileUtils.handleFileException( this.logPrefix(), @@ -1168,9 +1320,12 @@ export default class ChargingStation { configurationData.stationInfo = this.stationInfo; break; } + const measureId = `${FileType.ChargingStationConfiguration} write`; + const beginId = PerformanceStatistics.beginMeasure(measureId); const fileDescriptor = fs.openSync(this.configurationFile, 'w'); fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8'); fs.closeSync(fileDescriptor); + PerformanceStatistics.endMeasure(measureId, beginId); } catch (error) { FileUtils.handleFileException( this.logPrefix(), @@ -1193,8 +1348,8 @@ export default class ChargingStation { private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | null { let configuration: ChargingStationConfiguration = null; if (this.getOcppPersistentConfiguration()) { - configuration = - this.getConfigurationFromFile()?.configurationKey && this.getConfigurationFromFile(); + const configurationFromFile = this.getConfigurationFromFile(); + configuration = configurationFromFile?.configurationKey && configurationFromFile; } configuration && delete configuration.stationInfo; return configuration; @@ -1522,6 +1677,49 @@ export default class ChargingStation { return maxConnectors; } + private getMaximumAmperage(): number | undefined { + const maximumPower = (this.stationInfo['maxPower'] as number) ?? this.stationInfo.maximumPower; + switch (this.getCurrentOutType()) { + case CurrentType.AC: + return ACElectricUtils.amperagePerPhaseFromPower( + this.getNumberOfPhases(), + maximumPower / this.getNumberOfConnectors(), + this.getVoltageOut() + ); + case CurrentType.DC: + return DCElectricUtils.amperage(maximumPower, this.getVoltageOut()); + } + } + + private getAmperageLimitationUnitDivider(): number { + let unitDivider = 1; + switch (this.stationInfo.amperageLimitationUnit) { + case AmpereUnits.DECI_AMPERE: + unitDivider = 10; + break; + case AmpereUnits.CENTI_AMPERE: + unitDivider = 100; + break; + case AmpereUnits.MILLI_AMPERE: + unitDivider = 1000; + break; + } + return unitDivider; + } + + private getAmperageLimitation(): number | undefined { + if ( + this.stationInfo.amperageLimitationOcppKey && + this.getConfigurationKey(this.stationInfo.amperageLimitationOcppKey) + ) { + return ( + Utils.convertToInt( + this.getConfigurationKey(this.stationInfo.amperageLimitationOcppKey).value + ) / this.getAmperageLimitationUnitDivider() + ); + } + } + private async startMessageSequence(): Promise { if (this.stationInfo.autoRegister) { await this.ocppRequestService.sendMessageHandler( @@ -1884,7 +2082,7 @@ export default class ChargingStation { this.wsConnectionRestarted = true; } else if (this.getAutoReconnectMaxRetries() !== -1) { logger.error( - `${this.logPrefix()} WebSocket reconnect failure: max retries reached (${ + `${this.logPrefix()} WebSocket reconnect failure: maximum retries reached (${ this.autoReconnectRetryCount }) or retry disabled (${this.getAutoReconnectMaxRetries()})` );