X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2FChargingStation.ts;h=028de5918c9f38cc7d9dd4281b589611d19b19a4;hb=9f2e313013116428f5bce2be59e2f5c07502c026;hp=8b5b1ac2a2120616250873f203d5d2e1e1e458b2;hpb=1f5df42ad17d09d3a1f53f6618eba325a403d7ad;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 8b5b1ac2..028de591 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -5,7 +5,6 @@ import { BootNotificationResponse, RegistrationStatus } from '../types/ocpp/Resp import ChargingStationConfiguration, { ConfigurationKey } from '../types/ChargingStationConfiguration'; import ChargingStationTemplate, { CurrentType, PowerUnits, Voltage } from '../types/ChargingStationTemplate'; import { ConnectorPhaseRotation, StandardParametersKey, SupportedFeatureProfiles, VendorDefaultParametersKey } from '../types/ocpp/Configuration'; -import { ConnectorStatus, SampledValueTemplate } from '../types/Connectors'; import { MeterValueMeasurand, MeterValuePhase } from '../types/ocpp/MeterValues'; import { WSError, WebSocketCloseEventStatusCode } from '../types/WebSocket'; import WebSocket, { ClientOptions, Data, OPEN } from 'ws'; @@ -17,19 +16,23 @@ import ChargingStationInfo from '../types/ChargingStationInfo'; import { ChargingStationWorkerMessageEvents } from '../types/ChargingStationWorker'; import { ClientRequestArgs } from 'http'; import Configuration from '../utils/Configuration'; +import { ConnectorStatus } from '../types/ConnectorStatus'; import Constants from '../utils/Constants'; import { ErrorType } from '../types/ocpp/ErrorType'; import FileUtils from '../utils/FileUtils'; +import { JsonType } from '../types/JsonType'; import { MessageType } from '../types/ocpp/MessageType'; import OCPP16IncomingRequestService from './ocpp/1.6/OCPP16IncomingRequestService'; import OCPP16RequestService from './ocpp/1.6/OCPP16RequestService'; import OCPP16ResponseService from './ocpp/1.6/OCPP16ResponseService'; -import OCPPError from './ocpp/OCPPError'; +import OCPPError from '../exception/OCPPError'; import OCPPIncomingRequestService from './ocpp/OCPPIncomingRequestService'; import OCPPRequestService from './ocpp/OCPPRequestService'; import { OCPPVersion } from '../types/ocpp/OCPPVersion'; import PerformanceStatistics from '../performance/PerformanceStatistics'; +import { SampledValueTemplate } from '../types/MeasurandPerPhaseSampledValueTemplates'; import { StopTransactionReason } from '../types/ocpp/Transaction'; +import { SupervisionUrlDistribution } from '../types/ConfigurationData'; import { URL } from 'url'; import Utils from '../utils/Utils'; import crypto from 'crypto'; @@ -39,6 +42,7 @@ import { parentPort } from 'worker_threads'; import path from 'path'; export default class ChargingStation { + public readonly id: string; public readonly stationTemplateFile: string; public authorizedTags: string[]; public stationInfo!: ChargingStationInfo; @@ -63,18 +67,16 @@ export default class ChargingStation { private webSocketPingSetInterval!: NodeJS.Timeout; constructor(index: number, stationTemplateFile: string) { + this.id = Utils.generateUUID(); this.index = index; this.stationTemplateFile = stationTemplateFile; - this.connectors = new Map(); - this.initialize(); - this.stopped = false; this.wsConnectionRestarted = false; this.autoReconnectRetryCount = 0; - + this.connectors = new Map(); this.requests = new Map(); this.messageBuffer = new Set(); - + this.initialize(); this.authorizedTags = this.getAuthorizedTags(); } @@ -117,11 +119,31 @@ export default class ChargingStation { } public isWebSocketConnectionOpened(): boolean { - return this.wsConnection?.readyState === OPEN; + return this?.wsConnection?.readyState === OPEN; + } + + public getRegistrationStatus(): RegistrationStatus { + return this?.bootNotificationResponse?.status; + } + + public isInUnknownState(): boolean { + return Utils.isNullOrUndefined(this?.bootNotificationResponse?.status); + } + + public isInPendingState(): boolean { + return this?.bootNotificationResponse?.status === RegistrationStatus.PENDING; + } + + public isInAcceptedState(): boolean { + return this?.bootNotificationResponse?.status === RegistrationStatus.ACCEPTED; + } + + public isInRejectedState(): boolean { + return this?.bootNotificationResponse?.status === RegistrationStatus.REJECTED; } public isRegistered(): boolean { - return this.bootNotificationResponse?.status === RegistrationStatus.ACCEPTED; + return !this.isInUnknownState() && (this.isInAcceptedState() || this.isInPendingState()); } public isChargingStationAvailable(): boolean { @@ -129,7 +151,7 @@ export default class ChargingStation { } public isConnectorAvailable(id: number): boolean { - return this.getConnectorStatus(id).availability === AvailabilityType.OPERATIVE; + return id > 0 && this.getConnectorStatus(id).availability === AvailabilityType.OPERATIVE; } public getNumberOfConnectors(): number { @@ -144,6 +166,10 @@ export default class ChargingStation { return this.stationInfo.currentOutType ?? CurrentType.AC; } + public getOcppStrictCompliance(): boolean { + return this.stationInfo.ocppStrictCompliance ?? false; + } + public getVoltageOut(): number | undefined { const errMsg = `${this.logPrefix()} Unknown ${this.getCurrentOutType()} currentOutType in template file ${this.stationTemplateFile}, cannot define default voltage out`; let defaultVoltageOut: number; @@ -460,6 +486,10 @@ export default class ChargingStation { } catch (error) { FileUtils.handleFileException(this.logPrefix(), 'Template', this.stationTemplateFile, error as NodeJS.ErrnoException); } + const chargingStationId = this.getChargingStationId(stationTemplateFromFile); + // Deprecation template keys section + this.warnDeprecatedTemplateKey(stationTemplateFromFile, 'supervisionUrl', chargingStationId, 'Use \'supervisionUrls\' instead'); + this.convertDeprecatedTemplateKey(stationTemplateFromFile, 'supervisionUrl', 'supervisionUrls'); const stationInfo: ChargingStationInfo = stationTemplateFromFile ?? {} as ChargingStationInfo; stationInfo.wsOptions = stationTemplateFromFile?.wsOptions ?? {}; if (!Utils.isEmptyArray(stationTemplateFromFile.power)) { @@ -476,7 +506,7 @@ export default class ChargingStation { } delete stationInfo.power; delete stationInfo.powerUnit; - stationInfo.chargingStationId = this.getChargingStationId(stationTemplateFromFile); + stationInfo.chargingStationId = chargingStationId; stationInfo.resetTime = stationTemplateFromFile.resetTime ? stationTemplateFromFile.resetTime * 1000 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME; return stationInfo; } @@ -558,8 +588,8 @@ export default class ChargingStation { this.wsConfiguredConnectionUrl = new URL(this.getConfiguredSupervisionUrl().href + '/' + this.stationInfo.chargingStationId); switch (this.getOcppVersion()) { case OCPPVersion.VERSION_16: - this.ocppIncomingRequestService = new OCPP16IncomingRequestService(this); - this.ocppRequestService = new OCPP16RequestService(this, new OCPP16ResponseService(this)); + this.ocppIncomingRequestService = OCPP16IncomingRequestService.getInstance(this); + this.ocppRequestService = OCPP16RequestService.getInstance(this, OCPP16ResponseService.getInstance(this)); break; default: this.handleUnsupportedVersion(this.getOcppVersion()); @@ -576,7 +606,7 @@ export default class ChargingStation { } this.stationInfo.powerDivider = this.getPowerDivider(); if (this.getEnableStatistics()) { - this.performanceStatistics = new PerformanceStatistics(this.stationInfo.chargingStationId, this.wsConnectionUrl); + this.performanceStatistics = PerformanceStatistics.getInstance(this.id, this.stationInfo.chargingStationId, this.wsConnectionUrl); } } @@ -622,19 +652,19 @@ export default class ChargingStation { private async onOpen(): Promise { logger.info(`${this.logPrefix()} Connected to OCPP server through ${this.wsConnectionUrl.toString()}`); - if (!this.isRegistered()) { + if (!this.isInAcceptedState()) { // 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++; + if (!this.isInAcceptedState()) { + this.getRegistrationMaxRetries() !== -1 && 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)); + } while (!this.isInAcceptedState() && (registrationRetryCount <= this.getRegistrationMaxRetries() || this.getRegistrationMaxRetries() === -1)); } - if (this.isRegistered()) { + if (this.isInAcceptedState()) { await this.startMessageSequence(); this.stopped && (this.stopped = false); if (this.wsConnectionRestarted && this.isWebSocketConnectionOpened()) { @@ -665,10 +695,10 @@ export default class ChargingStation { private async onMessage(data: Data): Promise { let [messageType, messageId, commandName, commandPayload, errorDetails]: IncomingRequest = [0, '', '' as IncomingRequestCommand, {}, {}]; - let responseCallback: (payload: Record | string, requestPayload: Record) => void; + let responseCallback: (payload: JsonType | string, requestPayload: JsonType | OCPPError) => void; let rejectCallback: (error: OCPPError, requestStatistic?: boolean) => void; let requestCommandName: RequestCommand | IncomingRequestCommand; - let requestPayload: Record; + let requestPayload: JsonType | OCPPError; let cachedRequest: CachedRequest; let errMsg: string; try { @@ -829,7 +859,7 @@ export default class ChargingStation { } private getMaxNumberOfConnectors(): number { - let maxConnectors = 0; + let maxConnectors: number; if (!Utils.isEmptyArray(this.stationInfo.numberOfConnectors)) { const numberOfConnectors = this.stationInfo.numberOfConnectors as number[]; // Distribute evenly the number of connectors @@ -843,6 +873,10 @@ export default class ChargingStation { } private async startMessageSequence(): Promise { + if (this.stationInfo.autoRegister) { + await this.ocppRequestService.sendBootNotification(this.bootNotificationRequest.chargePointModel, + this.bootNotificationRequest.chargePointVendor, this.bootNotificationRequest.chargeBoxSerialNumber, this.bootNotificationRequest.firmwareVersion); + } // Start WebSocket ping this.startWebSocketPing(); // Start heartbeat @@ -890,8 +924,7 @@ export default class ChargingStation { this.stopHeartbeat(); // Stop the ATG if (this.stationInfo.AutomaticTransactionGenerator.enable && - this.automaticTransactionGenerator && - this.automaticTransactionGenerator.started) { + this.automaticTransactionGenerator?.started) { this.automaticTransactionGenerator.stop(); } else { for (const connectorId of this.connectors.keys()) { @@ -928,17 +961,45 @@ export default class ChargingStation { } } + private warnDeprecatedTemplateKey(template: ChargingStationTemplate, key: string, chargingStationId: string, logMsgToAppend = ''): void { + if (!Utils.isUndefined(template[key])) { + logger.warn(`${Utils.logPrefix(` ${chargingStationId} |`)} Deprecated template key '${key}' usage in file '${this.stationTemplateFile}'${logMsgToAppend && '. ' + logMsgToAppend}`); + } + } + + private convertDeprecatedTemplateKey(template: ChargingStationTemplate, deprecatedKey: string, key: string): void { + if (!Utils.isUndefined(template[deprecatedKey])) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + template[key] = template[deprecatedKey]; + delete template[deprecatedKey]; + } + } + private getConfiguredSupervisionUrl(): URL { - const supervisionUrls = Utils.cloneObject(this.stationInfo.supervisionUrl ?? Configuration.getSupervisionUrls()); - let indexUrl = 0; + const supervisionUrls = Utils.cloneObject(this.stationInfo.supervisionUrls ?? Configuration.getSupervisionUrls()); if (!Utils.isEmptyArray(supervisionUrls)) { - if (Configuration.getDistributeStationsToTenantsEqually()) { - indexUrl = this.index % supervisionUrls.length; - } else { - // Get a random url - indexUrl = Math.floor(Utils.secureRandom() * supervisionUrls.length); + let urlIndex = 0; + switch (Configuration.getSupervisionUrlDistribution()) { + case SupervisionUrlDistribution.ROUND_ROBIN: + urlIndex = (this.index - 1) % supervisionUrls.length; + break; + case SupervisionUrlDistribution.RANDOM: + // Get a random url + urlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length); + break; + case SupervisionUrlDistribution.SEQUENTIAL: + if (this.index <= supervisionUrls.length) { + urlIndex = this.index - 1; + } else { + logger.warn(`${this.logPrefix()} No more configured supervision urls available, using the first one`); + } + break; + default: + logger.error(`${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${SupervisionUrlDistribution.ROUND_ROBIN}`); + urlIndex = (this.index - 1) % supervisionUrls.length; + break; } - return new URL(supervisionUrls[indexUrl]); + return new URL(supervisionUrls[urlIndex]); } return new URL(supervisionUrls as string); } @@ -1054,8 +1115,7 @@ export default class ChargingStation { // Stop the ATG if needed if (this.stationInfo.AutomaticTransactionGenerator.enable && this.stationInfo.AutomaticTransactionGenerator.stopOnConnectionFailure && - this.automaticTransactionGenerator && - this.automaticTransactionGenerator.started) { + this.automaticTransactionGenerator?.started) { this.automaticTransactionGenerator.stop(); } if (this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() || this.getAutoReconnectMaxRetries() === -1) {