+ 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.wsConnectionUrl = new URL(this.getSupervisionURL().href + '/' + 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.wsConnectionUrl);
+ }
+ }
+
+ 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());
+ }
+ }
+
+ private async onOpen(): Promise<void> {
+ logger.info(`${this.logPrefix()} Connected to OCPP server through ${this.wsConnectionUrl.toString()}`);
+ if (!this.isRegistered()) {
+ // Send BootNotification
+ let registrationRetryCount = 0;
+ do {
+ this.bootNotificationResponse = await this.ocppRequestService.sendBootNotification(this.bootNotificationRequest.chargePointModel,
+ this.bootNotificationRequest.chargePointVendor, this.bootNotificationRequest.chargeBoxSerialNumber, this.bootNotificationRequest.firmwareVersion);
+ if (!this.isRegistered()) {
+ registrationRetryCount++;
+ await Utils.sleep(this.bootNotificationResponse?.interval ? this.bootNotificationResponse.interval * 1000 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL);
+ }
+ } while (!this.isRegistered() && (registrationRetryCount <= this.getRegistrationMaxRetries() || this.getRegistrationMaxRetries() === -1));
+ }
+ if (this.isRegistered()) {
+ await this.startMessageSequence();
+ this.hasStopped && (this.hasStopped = false);
+ if (this.hasSocketRestarted && this.isWebSocketConnectionOpened()) {
+ this.flushMessageQueue();
+ }
+ } else {
+ logger.error(`${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`);
+ }
+ this.autoReconnectRetryCount = 0;
+ this.hasSocketRestarted = false;
+ }
+
+ private async onClose(code: number): Promise<void> {
+ switch (code) {
+ case WebSocketCloseEventStatusCode.CLOSE_NORMAL: // Normal close
+ case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
+ logger.info(`${this.logPrefix()} Socket normally closed with status '${Utils.getWebSocketCloseEventStatusString(code)}'`);
+ this.autoReconnectRetryCount = 0;
+ break;
+ default: // Abnormal close
+ logger.error(`${this.logPrefix()} Socket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(code)}'`);
+ await this.reconnect(code);
+ break;
+ }
+ }
+
+ private async onMessage(data: Data): Promise<void> {
+ let [messageType, messageId, commandName, commandPayload, errorDetails]: IncomingRequest = [0, '', '' as IncomingRequestCommand, {}, {}];
+ let responseCallback: (payload: Record<string, unknown> | string, requestPayload: Record<string, unknown>) => void;
+ let rejectCallback: (error: OCPPError) => void;
+ let requestPayload: Record<string, unknown>;
+ let cachedRequest: Request;
+ let errMsg: string;
+ try {
+ const request = JSON.parse(data.toString()) as IncomingRequest;
+ if (Utils.isIterable(request)) {
+ // Parse the message
+ [messageType, messageId, commandName, commandPayload, errorDetails] = request;
+ } else {
+ throw new OCPPError(ErrorType.PROTOCOL_ERROR, 'Incoming request is not iterable', commandName);
+ }
+ // Check the Type of message
+ switch (messageType) {
+ // Incoming Message
+ case MessageType.CALL_MESSAGE:
+ if (this.getEnableStatistics()) {
+ this.performanceStatistics.addRequestStatistic(commandName, messageType);
+ }
+ // Process the call
+ await this.ocppIncomingRequestService.handleRequest(messageId, commandName, commandPayload);
+ break;
+ // Outcome Message
+ case MessageType.CALL_RESULT_MESSAGE: