+ public getConfigurationKey(key: string | StandardParametersKey, caseInsensitive = false): ConfigurationKey | undefined {
+ const configurationKey: ConfigurationKey | undefined = this.configuration.configurationKey.find((configElement) => {
+ if (caseInsensitive) {
+ return configElement.key.toLowerCase() === key.toLowerCase();
+ }
+ return configElement.key === key;
+ });
+ return configurationKey;
+ }
+
+ public addConfigurationKey(key: string | StandardParametersKey, value: string, readonly = false, visible = true, reboot = false): void {
+ const keyFound = this.getConfigurationKey(key);
+ if (!keyFound) {
+ this.configuration.configurationKey.push({
+ key,
+ readonly,
+ value,
+ visible,
+ reboot,
+ });
+ } else {
+ logger.error(`${this.logPrefix()} Trying to add an already existing configuration key: %j`, keyFound);
+ }
+ }
+
+ public setConfigurationKeyValue(key: string | StandardParametersKey, value: string): void {
+ const keyFound = this.getConfigurationKey(key);
+ if (keyFound) {
+ const keyIndex = this.configuration.configurationKey.indexOf(keyFound);
+ this.configuration.configurationKey[keyIndex].value = value;
+ } else {
+ logger.error(`${this.logPrefix()} Trying to set a value on a non existing configuration key: %j`, { key, value });
+ }
+ }
+
+ public setChargingProfile(connectorId: number, cp: ChargingProfile): void {
+ let cpReplaced = false;
+ if (!Utils.isEmptyArray(this.getConnector(connectorId).chargingProfiles)) {
+ this.getConnector(connectorId).chargingProfiles?.forEach((chargingProfile: ChargingProfile, index: number) => {
+ if (chargingProfile.chargingProfileId === cp.chargingProfileId
+ || (chargingProfile.stackLevel === cp.stackLevel && chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)) {
+ this.getConnector(connectorId).chargingProfiles[index] = cp;
+ cpReplaced = true;
+ }
+ });
+ }
+ !cpReplaced && this.getConnector(connectorId).chargingProfiles?.push(cp);
+ }
+
+ public resetTransactionOnConnector(connectorId: number): void {
+ this.getConnector(connectorId).authorized = false;
+ this.getConnector(connectorId).transactionStarted = false;
+ delete this.getConnector(connectorId).authorizeIdTag;
+ delete this.getConnector(connectorId).transactionId;
+ delete this.getConnector(connectorId).transactionIdTag;
+ this.getConnector(connectorId).transactionEnergyActiveImportRegisterValue = 0;
+ delete this.getConnector(connectorId).transactionBeginMeterValue;
+ this.stopMeterValues(connectorId);
+ }
+
+ public addToMessageQueue(message: string): void {
+ let dups = false;
+ // Handle dups in message queue
+ for (const bufferedMessage of this.messageQueue) {
+ // Message already in the queue
+ if (message === bufferedMessage) {
+ dups = true;
+ break;
+ }
+ }
+ if (!dups) {
+ // Queue message
+ this.messageQueue.push(message);
+ }
+ }
+
+ private flushMessageQueue() {
+ if (!Utils.isEmptyArray(this.messageQueue)) {
+ this.messageQueue.forEach((message, index) => {
+ this.messageQueue.splice(index, 1);
+ this.wsConnection.send(message);
+ });
+ }
+ }
+
+ private getChargingStationId(stationTemplate: ChargingStationTemplate): string {
+ // In case of multiple instances: add instance index to charging station id
+ let instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
+ instanceIndex = instanceIndex > 0 ? instanceIndex : '';
+ const idSuffix = stationTemplate.nameSuffix ?? '';
+ return stationTemplate.fixedName ? stationTemplate.baseName : stationTemplate.baseName + '-' + instanceIndex.toString() + ('000000000' + this.index.toString()).substr(('000000000' + this.index.toString()).length - 4) + idSuffix;
+ }
+
+ private buildStationInfo(): ChargingStationInfo {
+ let stationTemplateFromFile: ChargingStationTemplate;
+ try {
+ // Load template file
+ const fileDescriptor = fs.openSync(this.stationTemplateFile, 'r');
+ stationTemplateFromFile = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8')) as ChargingStationTemplate;
+ fs.closeSync(fileDescriptor);
+ } catch (error) {
+ FileUtils.handleFileException(this.logPrefix(), 'Template', this.stationTemplateFile, error);
+ }
+ const stationInfo: ChargingStationInfo = stationTemplateFromFile ?? {} as ChargingStationInfo;
+ if (!Utils.isEmptyArray(stationTemplateFromFile.power)) {
+ stationTemplateFromFile.power = stationTemplateFromFile.power as number[];
+ const powerArrayRandomIndex = Math.floor(Math.random() * stationTemplateFromFile.power.length);
+ stationInfo.maxPower = stationTemplateFromFile.powerUnit === PowerUnits.KILO_WATT
+ ? stationTemplateFromFile.power[powerArrayRandomIndex] * 1000
+ : stationTemplateFromFile.power[powerArrayRandomIndex];
+ } else {
+ stationTemplateFromFile.power = stationTemplateFromFile.power as number;
+ stationInfo.maxPower = stationTemplateFromFile.powerUnit === PowerUnits.KILO_WATT
+ ? stationTemplateFromFile.power * 1000
+ : stationTemplateFromFile.power;
+ }
+ delete stationInfo.power;
+ delete stationInfo.powerUnit;
+ stationInfo.chargingStationId = this.getChargingStationId(stationTemplateFromFile);
+ stationInfo.resetTime = stationTemplateFromFile.resetTime ? stationTemplateFromFile.resetTime * 1000 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
+ return stationInfo;
+ }
+
+ private getOCPPVersion(): OCPPVersion {
+ return this.stationInfo.ocppVersion ? this.stationInfo.ocppVersion : OCPPVersion.VERSION_16;
+ }
+
+ private handleUnsupportedVersion(version: OCPPVersion) {
+ const errMsg = `${this.logPrefix()} Unsupported protocol version '${version}' configured in template file ${this.stationTemplateFile}`;
+ logger.error(errMsg);
+ throw new Error(errMsg);
+ }
+
+ private initialize(): void {
+ this.stationInfo = this.buildStationInfo();
+ this.bootNotificationRequest = {
+ chargePointModel: this.stationInfo.chargePointModel,
+ chargePointVendor: this.stationInfo.chargePointVendor,
+ ...!Utils.isUndefined(this.stationInfo.chargeBoxSerialNumberPrefix) && { chargeBoxSerialNumber: this.stationInfo.chargeBoxSerialNumberPrefix },
+ ...!Utils.isUndefined(this.stationInfo.firmwareVersion) && { firmwareVersion: this.stationInfo.firmwareVersion },
+ };
+ this.configuration = this.getTemplateChargingStationConfiguration();
+ this.supervisionUrl = this.getSupervisionURL();
+ this.wsConnectionUrl = this.supervisionUrl + '/' + this.stationInfo.chargingStationId;
+ // Build connectors if needed
+ const maxConnectors = this.getMaxNumberOfConnectors();
+ if (maxConnectors <= 0) {
+ logger.warn(`${this.logPrefix()} Charging station template ${this.stationTemplateFile} with ${maxConnectors} connectors`);
+ }
+ const templateMaxConnectors = this.getTemplateMaxNumberOfConnectors();
+ if (templateMaxConnectors <= 0) {
+ logger.warn(`${this.logPrefix()} Charging station template ${this.stationTemplateFile} with no connector configuration`);
+ }
+ if (!this.stationInfo.Connectors[0]) {
+ logger.warn(`${this.logPrefix()} Charging station template ${this.stationTemplateFile} with no connector Id 0 configuration`);
+ }
+ // Sanity check
+ 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.stationTemplateFile}, forcing random connector configurations affectation`);
+ this.stationInfo.randomConnectors = true;
+ }
+ const connectorsConfigHash = crypto.createHash('sha256').update(JSON.stringify(this.stationInfo.Connectors) + maxConnectors.toString()).digest('hex');
+ // FIXME: Handle shrinking the number of connectors
+ if (!this.connectors || (this.connectors && this.connectorsConfigurationHash !== connectorsConfigHash)) {
+ this.connectorsConfigurationHash = connectorsConfigHash;
+ // Add connector Id 0
+ let lastConnector = '0';
+ for (lastConnector in this.stationInfo.Connectors) {
+ if (Utils.convertToInt(lastConnector) === 0 && this.getUseConnectorId0() && this.stationInfo.Connectors[lastConnector]) {
+ this.connectors[lastConnector] = Utils.cloneObject<Connector>(this.stationInfo.Connectors[lastConnector]);
+ this.connectors[lastConnector].availability = AvailabilityType.OPERATIVE;
+ if (Utils.isUndefined(this.connectors[lastConnector]?.chargingProfiles)) {
+ this.connectors[lastConnector].chargingProfiles = [];
+ }
+ }
+ }
+ // Generate all connectors
+ if ((this.stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0) {
+ for (let index = 1; index <= maxConnectors; index++) {
+ const randConnectorId = this.stationInfo.randomConnectors ? Utils.getRandomInt(Utils.convertToInt(lastConnector), 1) : index;
+ this.connectors[index] = Utils.cloneObject<Connector>(this.stationInfo.Connectors[randConnectorId]);
+ this.connectors[index].availability = AvailabilityType.OPERATIVE;
+ if (Utils.isUndefined(this.connectors[lastConnector]?.chargingProfiles)) {
+ this.connectors[index].chargingProfiles = [];
+ }
+ }
+ }
+ }
+ // Avoid duplication of connectors related information
+ delete this.stationInfo.Connectors;
+ // Initialize transaction attributes on connectors
+ for (const connector in this.connectors) {
+ if (Utils.convertToInt(connector) > 0 && !this.getConnector(Utils.convertToInt(connector)).transactionStarted) {
+ this.initTransactionAttributesOnConnector(Utils.convertToInt(connector));
+ }
+ }
+ switch (this.getOCPPVersion()) {
+ case OCPPVersion.VERSION_16:
+ this.ocppIncomingRequestService = new OCPP16IncomingRequestService(this);
+ this.ocppRequestService = new OCPP16RequestService(this, new OCPP16ResponseService(this));
+ break;
+ default:
+ this.handleUnsupportedVersion(this.getOCPPVersion());
+ break;
+ }
+ // OCPP parameters
+ this.initOCPPParameters();
+ if (this.stationInfo.autoRegister) {
+ this.bootNotificationResponse = {
+ currentTime: new Date().toISOString(),
+ interval: this.getHeartbeatInterval() / 1000,
+ status: RegistrationStatus.ACCEPTED
+ };
+ }
+ this.stationInfo.powerDivider = this.getPowerDivider();
+ if (this.getEnableStatistics()) {
+ this.performanceStatistics = new PerformanceStatistics(this.stationInfo.chargingStationId);
+ this.performanceObserver = new PerformanceObserver((list) => {
+ const entry = list.getEntries()[0];
+ this.performanceStatistics.logPerformance(entry, Constants.ENTITY_CHARGING_STATION);
+ this.performanceObserver.disconnect();
+ });
+ }
+ }
+
+ private initOCPPParameters(): void {
+ if (!this.getConfigurationKey(StandardParametersKey.SupportedFeatureProfiles)) {
+ this.addConfigurationKey(StandardParametersKey.SupportedFeatureProfiles, `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.Local_Auth_List_Management},${SupportedFeatureProfiles.Smart_Charging}`);
+ }
+ this.addConfigurationKey(StandardParametersKey.NumberOfConnectors, this.getNumberOfConnectors().toString(), true);
+ if (!this.getConfigurationKey(StandardParametersKey.MeterValuesSampledData)) {
+ this.addConfigurationKey(StandardParametersKey.MeterValuesSampledData, MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER);
+ }
+ if (!this.getConfigurationKey(StandardParametersKey.ConnectorPhaseRotation)) {
+ const connectorPhaseRotation = [];
+ for (const connector in this.connectors) {
+ // AC/DC
+ if (Utils.convertToInt(connector) === 0 && this.getNumberOfPhases() === 0) {
+ connectorPhaseRotation.push(`${connector}.${ConnectorPhaseRotation.RST}`);
+ } else if (Utils.convertToInt(connector) > 0 && this.getNumberOfPhases() === 0) {
+ connectorPhaseRotation.push(`${connector}.${ConnectorPhaseRotation.NotApplicable}`);
+ // AC
+ } else if (Utils.convertToInt(connector) > 0 && this.getNumberOfPhases() === 1) {
+ connectorPhaseRotation.push(`${connector}.${ConnectorPhaseRotation.NotApplicable}`);
+ } else if (Utils.convertToInt(connector) > 0 && this.getNumberOfPhases() === 3) {
+ connectorPhaseRotation.push(`${connector}.${ConnectorPhaseRotation.RST}`);
+ }
+ }
+ this.addConfigurationKey(StandardParametersKey.ConnectorPhaseRotation, connectorPhaseRotation.toString());
+ }
+ if (!this.getConfigurationKey(StandardParametersKey.AuthorizeRemoteTxRequests)) {
+ this.addConfigurationKey(StandardParametersKey.AuthorizeRemoteTxRequests, 'true');
+ }
+ if (!this.getConfigurationKey(StandardParametersKey.LocalAuthListEnabled)
+ && this.getConfigurationKey(StandardParametersKey.SupportedFeatureProfiles).value.includes(SupportedFeatureProfiles.Local_Auth_List_Management)) {
+ this.addConfigurationKey(StandardParametersKey.LocalAuthListEnabled, 'false');
+ }
+ if (!this.getConfigurationKey(StandardParametersKey.ConnectionTimeOut)) {
+ this.addConfigurationKey(StandardParametersKey.ConnectionTimeOut, Constants.DEFAULT_CONNECTION_TIMEOUT.toString());