X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2FChargingStation.ts;h=e217991ac8aad3c7c4256010687929df0f0162a3;hb=9388120364d5a2f3b9103f05802e1756d2dad04f;hp=926915fb016a5497fd519667dff1199052b666e2;hpb=16c46962f43a692c996b1c89a94dc572fea52d63;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 926915fb..e217991a 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -120,8 +120,9 @@ import { createSerialNumber, getAmperageLimitationUnitDivider, getBootConnectorStatus, - getChargingStationConnectorChargingProfilesPowerLimit, + getChargingStationChargingProfilesLimit, getChargingStationId, + getConnectorChargingProfilesLimit, getDefaultVoltageOut, getHashId, getIdTagsFile, @@ -131,6 +132,7 @@ import { hasFeatureProfile, hasReservationExpired, initializeConnectorsMapStatus, + prepareConnectorStatus, propagateSerialNumber, setChargingStationOptions, stationTemplateToStationInfo, @@ -181,7 +183,6 @@ export class ChargingStation extends EventEmitter { private ocppIncomingRequestService!: OCPPIncomingRequestService private readonly messageBuffer: Set private configuredSupervisionUrl!: URL - private wsConnectionRetried: boolean private wsConnectionRetryCount: number private templateFileWatcher?: FSWatcher private templateFileHash!: string @@ -196,7 +197,6 @@ export class ChargingStation extends EventEmitter { this.starting = false this.stopping = false this.wsConnection = null - this.wsConnectionRetried = false this.wsConnectionRetryCount = 0 this.index = index this.templateFile = templateFile @@ -225,16 +225,21 @@ export class ChargingStation extends EventEmitter { }) this.on(ChargingStationEvents.accepted, () => { this.startMessageSequence( - this.wsConnectionRetried + this.wsConnectionRetryCount > 0 ? true : this.getAutomaticTransactionGeneratorConfiguration()?.stopAbsoluteDuration ).catch((error: unknown) => { logger.error(`${this.logPrefix()} Error while starting the message sequence:`, error) }) - this.wsConnectionRetried = false + this.wsConnectionRetryCount = 0 }) this.on(ChargingStationEvents.rejected, () => { - this.wsConnectionRetried = false + this.wsConnectionRetryCount = 0 + }) + this.on(ChargingStationEvents.connected, () => { + if (this.wsPingSetInterval == null) { + this.startWebSocketPing() + } }) this.on(ChargingStationEvents.disconnected, () => { try { @@ -261,14 +266,17 @@ export class ChargingStation extends EventEmitter { } public get wsConnectionUrl (): URL { + const wsConnectionBaseUrlStr = `${ + this.stationInfo?.supervisionUrlOcppConfiguration === true && + isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) && + isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value) + ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value + : this.configuredSupervisionUrl.href + }` return new URL( - `${ - this.stationInfo?.supervisionUrlOcppConfiguration === true && - isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) && - isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value) - ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value - : this.configuredSupervisionUrl.href - }/${this.stationInfo?.chargingStationId}` + `${wsConnectionBaseUrlStr}${ + !wsConnectionBaseUrlStr.endsWith('/') ? '/' : '' + }${this.stationInfo?.chargingStationId}` ) } @@ -384,14 +392,14 @@ export class ChargingStation extends EventEmitter { } public getConnectorMaximumAvailablePower (connectorId: number): number { - let connectorAmperageLimitationPowerLimit: number | undefined + let connectorAmperageLimitationLimit: number | undefined const amperageLimitation = this.getAmperageLimitation() if ( amperageLimitation != null && // eslint-disable-next-line @typescript-eslint/no-non-null-assertion amperageLimitation < this.stationInfo!.maximumAmperage! ) { - connectorAmperageLimitationPowerLimit = + connectorAmperageLimitationLimit = (this.stationInfo?.currentOutType === CurrentType.AC ? ACElectricUtils.powerTotal( this.getNumberOfPhases(), @@ -407,17 +415,25 @@ export class ChargingStation extends EventEmitter { } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const connectorMaximumPower = this.stationInfo!.maximumPower! / this.powerDivider! - const connectorChargingProfilesPowerLimit = - getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId) + const chargingStationChargingProfilesLimit = + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + getChargingStationChargingProfilesLimit(this)! / this.powerDivider! + const connectorChargingProfilesLimit = getConnectorChargingProfilesLimit(this, connectorId) return min( - isNaN(connectorMaximumPower) ? Infinity : connectorMaximumPower, + isNaN(connectorMaximumPower) ? Number.POSITIVE_INFINITY : connectorMaximumPower, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - isNaN(connectorAmperageLimitationPowerLimit!) - ? Infinity + isNaN(connectorAmperageLimitationLimit!) + ? Number.POSITIVE_INFINITY : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - connectorAmperageLimitationPowerLimit!, + connectorAmperageLimitationLimit!, + isNaN(chargingStationChargingProfilesLimit) + ? Number.POSITIVE_INFINITY + : chargingStationChargingProfilesLimit, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - isNaN(connectorChargingProfilesPowerLimit!) ? Infinity : connectorChargingProfilesPowerLimit! + isNaN(connectorChargingProfilesLimit!) + ? Number.POSITIVE_INFINITY + : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + connectorChargingProfilesLimit! ) } @@ -548,7 +564,8 @@ export class ChargingStation extends EventEmitter { } public startHeartbeat (): void { - if (this.getHeartbeatInterval() > 0 && this.heartbeatSetInterval == null) { + const heartbeatInterval = this.getHeartbeatInterval() + if (heartbeatInterval > 0 && this.heartbeatSetInterval == null) { this.heartbeatSetInterval = setInterval(() => { this.ocppRequestService .requestHandler(this, RequestCommand.HEARTBEAT) @@ -558,21 +575,21 @@ export class ChargingStation extends EventEmitter { error ) }) - }, this.getHeartbeatInterval()) + }, heartbeatInterval) logger.info( `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds( - this.getHeartbeatInterval() + heartbeatInterval )}` ) } else if (this.heartbeatSetInterval != null) { logger.info( `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds( - this.getHeartbeatInterval() + heartbeatInterval )}` ) } else { logger.error( - `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, not starting the heartbeat` + `${this.logPrefix()} Heartbeat interval set to ${heartbeatInterval}, not starting the heartbeat` ) } } @@ -660,6 +677,11 @@ export class ChargingStation extends EventEmitter { } } + public restartMeterValues (connectorId: number, interval: number): void { + this.stopMeterValues(connectorId) + this.startMeterValues(connectorId, interval) + } + private add (): void { this.emit(ChargingStationEvents.added) } @@ -943,7 +965,7 @@ export class ChargingStation extends EventEmitter { ) } return await this.ocppRequestService.requestHandler< - StopTransactionRequest, + Partial, StopTransactionResponse >(this, RequestCommand.STOP_TRANSACTION, { transactionId, @@ -1358,7 +1380,9 @@ export class ChargingStation extends EventEmitter { addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0') } if (getConfigurationKey(this, StandardParametersKey.HeartBeatInterval) == null) { - addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { visible: false }) + addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { + visible: false + }) } if ( this.stationInfo?.supervisionUrlOcppConfiguration === true && @@ -1376,7 +1400,9 @@ export class ChargingStation extends EventEmitter { isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) && getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) != null ) { - deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey, { save: false }) + deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey, { + save: false + }) } if ( isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) && @@ -1458,7 +1484,10 @@ export class ChargingStation extends EventEmitter { private initializeConnectorsOrEvsesFromFile (configuration: ChargingStationConfiguration): void { if (configuration.connectorsStatus != null && configuration.evsesStatus == null) { for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) { - this.connectors.set(connectorId, clone(connectorStatus)) + this.connectors.set( + connectorId, + prepareConnectorStatus(clone(connectorStatus)) + ) } } else if (configuration.evsesStatus != null && configuration.connectorsStatus == null) { for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) { @@ -1470,7 +1499,7 @@ export class ChargingStation extends EventEmitter { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [ connectorId, - connectorStatus + prepareConnectorStatus(connectorStatus) ]) ) }) @@ -1735,7 +1764,9 @@ export class ChargingStation extends EventEmitter { ...(this.connectors.size > 0 && { connectorsStatus: configurationData.connectorsStatus }), - ...(this.evses.size > 0 && { evsesStatus: configurationData.evsesStatus }) + ...(this.evses.size > 0 && { + evsesStatus: configurationData.evsesStatus + }) } satisfies ChargingStationConfiguration) ) .digest('hex') @@ -1810,27 +1841,27 @@ export class ChargingStation extends EventEmitter { private async onOpen (): Promise { if (this.isWebSocketConnectionOpened()) { + this.emit(ChargingStationEvents.connected) this.emit(ChargingStationEvents.updated) logger.info( - `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} succeeded` + `${this.logPrefix()} Connection to OCPP server through ${ + this.wsConnectionUrl.href + } succeeded` ) let registrationRetryCount = 0 if (!this.isRegistered()) { // Send BootNotification do { - this.bootNotificationResponse = await this.ocppRequestService.requestHandler< + await this.ocppRequestService.requestHandler< BootNotificationRequest, BootNotificationResponse >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, { skipBufferingOnError: true }) - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (this.bootNotificationResponse?.currentTime != null) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.bootNotificationResponse.currentTime = convertToDate( - this.bootNotificationResponse.currentTime - )! - } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.bootNotificationResponse!.currentTime = convertToDate( + this.bootNotificationResponse?.currentTime + )! if (!this.isRegistered()) { this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount await sleep( @@ -1847,22 +1878,13 @@ export class ChargingStation extends EventEmitter { this.stationInfo?.registrationMaxRetries === -1) ) } - if (this.isRegistered()) { - this.emit(ChargingStationEvents.registered) - if (this.inAcceptedState()) { - this.emit(ChargingStationEvents.accepted) - } - } else { - if (this.inRejectedState()) { - this.emit(ChargingStationEvents.rejected) - } + if (!this.isRegistered()) { logger.error( `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount}) or retry disabled (${ this.stationInfo?.registrationMaxRetries })` ) } - this.wsConnectionRetryCount = 0 this.emit(ChargingStationEvents.updated) } else { logger.warn( @@ -1914,7 +1936,7 @@ export class ChargingStation extends EventEmitter { } throw new OCPPError( ErrorType.PROTOCOL_ERROR, - `Cached request for message id ${messageId} ${getMessageTypeString( + `Cached request for message id '${messageId}' ${getMessageTypeString( messageType )} is not an array`, undefined, @@ -1924,6 +1946,14 @@ export class ChargingStation extends EventEmitter { private async handleIncomingMessage (request: IncomingRequest): Promise { const [messageType, messageId, commandName, commandPayload] = request + if (this.requests.has(messageId)) { + throw new OCPPError( + ErrorType.SECURITY_ERROR, + `Received message with duplicate message id '${messageId}'`, + commandName, + commandPayload + ) + } if (this.stationInfo?.enableStatistics === true) { this.performanceStatistics?.addRequestStatistic(commandName, messageType) } @@ -1948,7 +1978,7 @@ export class ChargingStation extends EventEmitter { // Error throw new OCPPError( ErrorType.INTERNAL_ERROR, - `Response for unknown message id ${messageId}`, + `Response for unknown message id '${messageId}'`, undefined, commandPayload ) @@ -1973,7 +2003,7 @@ export class ChargingStation extends EventEmitter { // Error throw new OCPPError( ErrorType.INTERNAL_ERROR, - `Error response for unknown message id ${messageId}`, + `Error response for unknown message id '${messageId}'`, undefined, { errorType, errorMessage, errorDetails } ) @@ -2058,10 +2088,10 @@ export class ChargingStation extends EventEmitter { } if (!(error instanceof OCPPError)) { logger.warn( - `${this.logPrefix()} Error thrown at incoming OCPP command '${ + `${this.logPrefix()} Error thrown at incoming OCPP command ${ commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND // eslint-disable-next-line @typescript-eslint/no-base-to-string - }' message '${data.toString()}' handling is not an OCPPError:`, + } message '${data.toString()}' handling is not an OCPPError:`, error ) } @@ -2071,7 +2101,9 @@ export class ChargingStation extends EventEmitter { // eslint-disable-next-line @typescript-eslint/no-base-to-string }' message '${data.toString()}'${ this.requests.has(messageId) - ? ` matching cached request '${JSON.stringify(this.getCachedRequest(messageType, messageId))}'` + ? ` matching cached request '${JSON.stringify( + this.getCachedRequest(messageType, messageId) + )}'` : '' } processing error:`, error @@ -2213,29 +2245,40 @@ export class ChargingStation extends EventEmitter { }) } // Start WebSocket ping - this.startWebSocketPing() + if (this.wsPingSetInterval == null) { + this.startWebSocketPing() + } // Start heartbeat - this.startHeartbeat() + if (this.heartbeatSetInterval == null) { + this.startHeartbeat() + } // Initialize connectors status if (this.hasEvses) { for (const [evseId, evseStatus] of this.evses) { if (evseId > 0) { for (const [connectorId, connectorStatus] of evseStatus.connectors) { - const connectorBootStatus = getBootConnectorStatus(this, connectorId, connectorStatus) - await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus, evseId) + await sendAndSetConnectorStatus( + this, + connectorId, + getBootConnectorStatus(this, connectorId, connectorStatus), + evseId + ) } } } } else { for (const connectorId of this.connectors.keys()) { if (connectorId > 0) { - const connectorBootStatus = getBootConnectorStatus( + await sendAndSetConnectorStatus( this, connectorId, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.getConnectorStatus(connectorId)! + getBootConnectorStatus( + this, + connectorId, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.getConnectorStatus(connectorId)! + ) ) - await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus) } } } @@ -2298,13 +2341,14 @@ export class ChargingStation extends EventEmitter { } } + private getWebSocketPingInterval (): number { + return getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null + ? convertToInt(getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value) + : 0 + } + private startWebSocketPing (): void { - const webSocketPingInterval = - getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null - ? convertToInt( - getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value - ) - : 0 + const webSocketPingInterval = this.getWebSocketPingInterval() if (webSocketPingInterval > 0 && this.wsPingSetInterval == null) { this.wsPingSetInterval = setInterval(() => { if (this.isWebSocketConnectionOpened()) { @@ -2394,7 +2438,6 @@ export class ChargingStation extends EventEmitter { this.wsConnectionRetryCount < this.stationInfo!.autoReconnectMaxRetries! || this.stationInfo?.autoReconnectMaxRetries === -1 ) { - this.wsConnectionRetried = true ++this.wsConnectionRetryCount const reconnectDelay = this.stationInfo?.reconnectExponentialDelay === true @@ -2421,9 +2464,7 @@ export class ChargingStation extends EventEmitter { ) } else if (this.stationInfo?.autoReconnectMaxRetries !== -1) { logger.error( - `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${ - this.wsConnectionRetryCount - }) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries})` + `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${this.wsConnectionRetryCount.toString()}) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries?.toString()})` ) } }