// Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
+import { ACElectricUtils, DCElectricUtils } from '../utils/ElectricUtils';
import {
AvailabilityType,
BootNotificationRequest,
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,
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';
: 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) {
): 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(
}
}
+ 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)) {
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(),
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;
}
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',
if (stationInfoFromFile?.hash === stationInfoFromTemplate.hash) {
return stationInfoFromFile;
}
+ this.createSerialNumber(stationInfoFromTemplate, stationInfoFromFile);
return stationInfoFromTemplate;
}
};
}
- 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');
}
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) {
}
}
}
- // 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()) {
) {
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,
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(),
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(),
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;
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<void> {
if (this.stationInfo.autoRegister) {
await this.ocppRequestService.sendMessageHandler<BootNotificationResponse>(
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()})`
);