From 404ba5d9851fbaa7eeeffe912a34e5c76bc3a6fd Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sat, 28 Mar 2026 21:15:44 +0100 Subject: [PATCH] refactor: encapsulate connector/EVSE iteration behind generator API Introduce iterateConnectors() and iterateEvses() generators on ChargingStation, replacing ~30 duplicated if(hasEvses)/else iteration patterns across the codebase. Make connectors and evses Maps private to enforce usage of the new API. - Add ConnectorEntry and EvseEntry as unified types for both iteration and serialization, removing duplicate ConnectorDataEntry/EvseDataEntry - Add hasEvse() accessor method for EVSE existence checks - Fix remoteStartId not propagated in TransactionEvent (F01.FR.25) - Fix messagesInQueue in GetTransactionStatus to check actual queue - Fix OCPP 1.6 changeAvailability losing SCHEDULED response - Consolidate ad-hoc test mock factories to use createMockChargingStation - Update UI wire format and components for unified types BREAKING CHANGE: connectors and evses Maps are now private on ChargingStation. Use iterateConnectors(), iterateEvses(), hasEvse(), getEvseStatus(), getConnectorStatus() instead of direct Map access. --- .../AutomaticTransactionGenerator.ts | 48 +-- src/charging-station/ChargingStation.ts | 207 ++++-------- src/charging-station/Helpers.ts | 40 +-- .../ocpp/1.6/OCPP16IncomingRequestService.ts | 146 ++------ .../ocpp/1.6/OCPP16ResponseService.ts | 26 +- .../ocpp/2.0/OCPP20IncomingRequestService.ts | 290 ++++++++-------- .../ocpp/2.0/OCPP20ServiceUtils.ts | 41 ++- src/charging-station/ocpp/OCPPServiceUtils.ts | 61 +--- src/types/ChargingStationWorker.ts | 15 +- src/types/ConnectorStatus.ts | 6 + src/types/Evse.ts | 5 + src/types/index.ts | 6 +- .../ChargingStationConfigurationUtils.ts | 144 ++++---- .../ChargingStation-Lifecycle.test.ts | 10 +- .../ChargingStation-Resilience.test.ts | 4 +- .../charging-station/ChargingStation.test.ts | 6 +- tests/charging-station/Helpers.test.ts | 6 +- .../helpers/StationHelpers.ts | 40 ++- ...comingRequestService-SmartCharging.test.ts | 2 +- .../OCPP16Integration-Transactions.test.ts | 10 +- ...OCPP16ResponseService-Transactions.test.ts | 10 +- .../ocpp/1.6/OCPP16TestUtils.ts | 3 +- ...gRequestService-ChangeAvailability.test.ts | 20 +- ...mingRequestService-RemoteStartAuth.test.ts | 40 +-- ...estService-RequestStartTransaction.test.ts | 6 + ...OCPP20IncomingRequestService-Reset.test.ts | 6 +- ...mingRequestService-UnlockConnector.test.ts | 4 +- ...omingRequestService-UpdateFirmware.test.ts | 2 +- .../ocpp/2.0/OCPP20TestUtils.ts | 5 +- .../ChargingStationConfigurationUtils.test.ts | 318 +++++++++++------- tests/utils/MessageChannelUtils.test.ts | 75 ++--- .../components/charging-stations/CSData.vue | 20 +- ui/web/src/types/ChargingStationType.ts | 9 +- ui/web/tests/unit/CSData.test.ts | 28 +- ui/web/tests/unit/constants.ts | 8 +- 35 files changed, 739 insertions(+), 928 deletions(-) diff --git a/src/charging-station/AutomaticTransactionGenerator.ts b/src/charging-station/AutomaticTransactionGenerator.ts index 4b81f25e..654e7583 100644 --- a/src/charging-station/AutomaticTransactionGenerator.ts +++ b/src/charging-station/AutomaticTransactionGenerator.ts @@ -255,20 +255,8 @@ export class AutomaticTransactionGenerator { } private initializeConnectorsStatus (): void { - if (this.chargingStation.hasEvses) { - for (const [evseId, evseStatus] of this.chargingStation.evses) { - if (evseId > 0) { - for (const connectorId of evseStatus.connectors.keys()) { - this.connectorsStatus.set(connectorId, this.getConnectorStatus(connectorId)) - } - } - } - } else { - for (const connectorId of this.chargingStation.connectors.keys()) { - if (connectorId > 0) { - this.connectorsStatus.set(connectorId, this.getConnectorStatus(connectorId)) - } - } + for (const { connectorId } of this.chargingStation.iterateConnectors(true)) { + this.connectorsStatus.set(connectorId, this.getConnectorStatus(connectorId)) } } @@ -400,20 +388,8 @@ export class AutomaticTransactionGenerator { this.connectorsStatus.clear() this.initializeConnectorsStatus() } - if (this.chargingStation.hasEvses) { - for (const [evseId, evseStatus] of this.chargingStation.evses) { - if (evseId > 0) { - for (const connectorId of evseStatus.connectors.keys()) { - this.startConnector(connectorId, stopAbsoluteDuration) - } - } - } - } else { - for (const connectorId of this.chargingStation.connectors.keys()) { - if (connectorId > 0) { - this.startConnector(connectorId, stopAbsoluteDuration) - } - } + for (const { connectorId } of this.chargingStation.iterateConnectors(true)) { + this.startConnector(connectorId, stopAbsoluteDuration) } } @@ -466,20 +442,8 @@ export class AutomaticTransactionGenerator { } private stopConnectors (): void { - if (this.chargingStation.hasEvses) { - for (const [evseId, evseStatus] of this.chargingStation.evses) { - if (evseId > 0) { - for (const connectorId of evseStatus.connectors.keys()) { - this.stopConnector(connectorId) - } - } - } - } else { - for (const connectorId of this.chargingStation.connectors.keys()) { - if (connectorId > 0) { - this.stopConnector(connectorId) - } - } + for (const { connectorId } of this.chargingStation.iterateConnectors(true)) { + this.stopConnector(connectorId) } } diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index b1223cdd..c5da8219 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -23,12 +23,14 @@ import { type ChargingStationOcppConfiguration, type ChargingStationOptions, type ChargingStationTemplate, + type ConnectorEntry, type ConnectorStatus, ConnectorStatusEnum, CurrentType, type ErrorCallback, type ErrorResponse, ErrorType, + type EvseEntry, type EvseStatus, type EvseStatusConfiguration, FileType, @@ -163,8 +165,6 @@ export class ChargingStation extends EventEmitter { public automaticTransactionGenerator?: AutomaticTransactionGenerator public bootNotificationRequest?: BootNotificationRequest public bootNotificationResponse?: BootNotificationResponse - public readonly connectors: Map - public readonly evses: Map public heartbeatSetInterval?: NodeJS.Timeout public idTagsCache: IdTagsCache public readonly index: number @@ -205,7 +205,9 @@ export class ChargingStation extends EventEmitter { private configurationFile!: string private configurationFileHash!: string private configuredSupervisionUrl!: URL + private readonly connectors: Map private connectorsConfigurationHash!: string + private readonly evses: Map private evsesConfigurationHash!: string private flushingMessageBuffer: boolean private flushMessageBufferSetInterval?: NodeJS.Timeout @@ -464,21 +466,10 @@ export class ChargingStation extends EventEmitter { ): number | undefined { if (transactionId == null) { return undefined - } else if (this.hasEvses) { - for (const evseStatus of this.evses.values()) { - for (const [connectorId, connectorStatus] of evseStatus.connectors) { - if (connectorStatus.transactionId === transactionId) { - return connectorId - } - } - } - } else { - for (const connectorId of this.connectors.keys()) { - if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) { - return connectorId - } - } } + return this.iterateConnectors().find( + ({ connectorStatus }) => connectorStatus.transactionId === transactionId + )?.connectorId } /** @@ -596,16 +587,10 @@ export class ChargingStation extends EventEmitter { public getEvseIdByTransactionId (transactionId: number | string | undefined): number | undefined { if (transactionId == null) { return undefined - } else if (this.hasEvses) { - for (const [evseId, evseStatus] of this.evses) { - for (const connectorStatus of evseStatus.connectors.values()) { - if (connectorStatus.transactionId === transactionId) { - return evseId - } - } - } } - return undefined + return this.iterateConnectors().find( + ({ connectorStatus }) => connectorStatus.transactionId === transactionId + )?.evseId } /** @@ -678,26 +663,11 @@ export class ChargingStation extends EventEmitter { * @returns The number of running transactions */ public getNumberOfRunningTransactions (): number { - let numberOfRunningTransactions = 0 - if (this.hasEvses) { - for (const [evseId, evseStatus] of this.evses) { - if (evseId === 0) { - continue - } - for (const connectorStatus of evseStatus.connectors.values()) { - if (connectorStatus.transactionStarted === true) { - ++numberOfRunningTransactions - } - } - } - } else { - for (const connectorId of this.connectors.keys()) { - if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) { - ++numberOfRunningTransactions - } - } - } - return numberOfRunningTransactions + return this.iterateConnectors(true).reduce( + (count, { connectorStatus }) => + connectorStatus.transactionStarted === true ? count + 1 : count, + 0 + ) } /** @@ -710,21 +680,9 @@ export class ChargingStation extends EventEmitter { filterKey: ReservationKey, value: number | string ): Reservation | undefined { - if (this.hasEvses) { - for (const evseStatus of this.evses.values()) { - for (const connectorStatus of evseStatus.connectors.values()) { - if (connectorStatus.reservation?.[filterKey] === value) { - return connectorStatus.reservation - } - } - } - } else { - for (const connectorStatus of this.connectors.values()) { - if (connectorStatus.reservation?.[filterKey] === value) { - return connectorStatus.reservation - } - } - } + return this.iterateConnectors().find( + ({ connectorStatus }) => connectorStatus.reservation?.[filterKey] === value + )?.connectorStatus.reservation } public getReserveConnectorZeroSupported (): boolean { @@ -739,21 +697,9 @@ export class ChargingStation extends EventEmitter { * @returns The ID tag or undefined if not found */ public getTransactionIdTag (transactionId: number): string | undefined { - if (this.hasEvses) { - for (const evseStatus of this.evses.values()) { - for (const connectorStatus of evseStatus.connectors.values()) { - if (connectorStatus.transactionId === transactionId) { - return connectorStatus.transactionIdTag - } - } - } - } else { - for (const connectorId of this.connectors.keys()) { - if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) { - return this.getConnectorStatus(connectorId)?.transactionIdTag - } - } - } + return this.iterateConnectors().find( + ({ connectorStatus }) => connectorStatus.transactionId === transactionId + )?.connectorStatus.transactionIdTag } public getVoltageOut (stationInfo?: ChargingStationInfo): Voltage { @@ -781,6 +727,10 @@ export class ChargingStation extends EventEmitter { return this.connectors.has(connectorId) } + public hasEvse (evseId: number): boolean { + return this.evses.has(evseId) + } + public hasIdTags (): boolean { const idTagsFile = this.stationInfo != null ? getIdTagsFile(this.stationInfo) : undefined return idTagsFile != null && isNotEmptyArray(this.idTagsCache.getIdTags(idTagsFile)) @@ -846,6 +796,30 @@ export class ChargingStation extends EventEmitter { return this.wsConnection?.readyState === WebSocket.OPEN } + public * iterateConnectors (skipZero = false): Generator { + if (this.hasEvses) { + for (const [evseId, evseStatus] of this.evses) { + if (skipZero && evseId === 0) continue + for (const [connectorId, connectorStatus] of evseStatus.connectors) { + if (skipZero && connectorId === 0) continue + yield { connectorId, connectorStatus, evseId } + } + } + } else { + for (const [connectorId, connectorStatus] of this.connectors) { + if (skipZero && connectorId === 0) continue + yield { connectorId, connectorStatus, evseId: undefined } + } + } + } + + public * iterateEvses (skipZero = false): Generator { + for (const [evseId, evseStatus] of this.evses) { + if (skipZero && evseId === 0) continue + yield { evseId, evseStatus } + } + } + public lockConnector (connectorId: number): void { if (connectorId === 0) { logger.warn(`${this.logPrefix()} lockConnector: connector id 0 is not a physical connector`) @@ -2118,21 +2092,10 @@ export class ChargingStation extends EventEmitter { } if (getConfigurationKey(this, StandardParametersKey.ConnectorPhaseRotation) == null) { const connectorsPhaseRotation: string[] = [] - if (this.hasEvses) { - for (const evseStatus of this.evses.values()) { - for (const connectorId of evseStatus.connectors.keys()) { - const phaseRotation = getPhaseRotationValue(connectorId, this.getNumberOfPhases()) - if (phaseRotation != null) { - connectorsPhaseRotation.push(phaseRotation) - } - } - } - } else { - for (const connectorId of this.connectors.keys()) { - const phaseRotation = getPhaseRotationValue(connectorId, this.getNumberOfPhases()) - if (phaseRotation != null) { - connectorsPhaseRotation.push(phaseRotation) - } + for (const { connectorId } of this.iterateConnectors()) { + const phaseRotation = getPhaseRotationValue(connectorId, this.getNumberOfPhases()) + if (phaseRotation != null) { + connectorsPhaseRotation.push(phaseRotation) } } addConfigurationKey( @@ -2650,34 +2613,12 @@ export class ChargingStation extends EventEmitter { 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) { - await sendAndSetConnectorStatus(this, { - connectorId, - evseId, - status: getBootConnectorStatus(this, connectorId, connectorStatus), - } as unknown as StatusNotificationRequest) - } - } - } - } else { - for (const connectorId of this.connectors.keys()) { - if (connectorId > 0) { - const connectorStatus = this.getConnectorStatus(connectorId) - if (connectorStatus == null) { - logger.error( - `${this.logPrefix()} No connector ${connectorId.toString()} status found during message sequence start` - ) - continue - } - await sendAndSetConnectorStatus(this, { - connectorId, - status: getBootConnectorStatus(this, connectorId, connectorStatus), - } as unknown as StatusNotificationRequest) - } - } + for (const { connectorId, connectorStatus, evseId } of this.iterateConnectors(true)) { + await sendAndSetConnectorStatus(this, { + connectorId, + ...(evseId != null && { evseId }), + status: getBootConnectorStatus(this, connectorId, connectorStatus), + } as unknown as StatusNotificationRequest) } if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) { await this.ocppRequestService.requestHandler< @@ -2739,29 +2680,13 @@ export class ChargingStation extends EventEmitter { this.internalStopMessageSequence() // Stop ongoing transactions stopTransactions && (await stopRunningTransactions(this, reason)) - if (this.hasEvses) { - for (const [evseId, evseStatus] of this.evses) { - if (evseId > 0) { - for (const [connectorId, connectorStatus] of evseStatus.connectors) { - await sendAndSetConnectorStatus(this, { - connectorId, - evseId, - status: ConnectorStatusEnum.Unavailable, - } as unknown as StatusNotificationRequest) - delete connectorStatus.status - } - } - } - } else { - for (const connectorId of this.connectors.keys()) { - if (connectorId > 0) { - await sendAndSetConnectorStatus(this, { - connectorId, - status: ConnectorStatusEnum.Unavailable, - } as unknown as StatusNotificationRequest) - delete this.getConnectorStatus(connectorId)?.status - } - } + for (const { connectorId, connectorStatus, evseId } of this.iterateConnectors(true)) { + await sendAndSetConnectorStatus(this, { + connectorId, + ...(evseId != null && { evseId }), + status: ConnectorStatusEnum.Unavailable, + } as unknown as StatusNotificationRequest) + delete connectorStatus.status } } diff --git a/src/charging-station/Helpers.ts b/src/charging-station/Helpers.ts index b2bf0cc8..0f498689 100644 --- a/src/charging-station/Helpers.ts +++ b/src/charging-station/Helpers.ts @@ -121,19 +121,9 @@ export const hasPendingReservation = (connectorStatus: ConnectorStatus): boolean * @returns true if any connector has a pending reservation, false otherwise */ export const hasPendingReservations = (chargingStation: ChargingStation): boolean => { - if (chargingStation.hasEvses) { - for (const evseStatus of chargingStation.evses.values()) { - for (const connectorStatus of evseStatus.connectors.values()) { - if (hasPendingReservation(connectorStatus)) { - return true - } - } - } - } else { - for (const connectorStatus of chargingStation.connectors.values()) { - if (hasPendingReservation(connectorStatus)) { - return true - } + for (const { connectorStatus } of chargingStation.iterateConnectors()) { + if (hasPendingReservation(connectorStatus)) { + return true } } return false @@ -143,25 +133,9 @@ export const removeExpiredReservations = async ( chargingStation: ChargingStation ): Promise => { const reservations: Reservation[] = [] - if (chargingStation.hasEvses) { - for (const evseStatus of chargingStation.evses.values()) { - for (const connectorStatus of evseStatus.connectors.values()) { - if ( - connectorStatus.reservation != null && - hasReservationExpired(connectorStatus.reservation) - ) { - reservations.push(connectorStatus.reservation) - } - } - } - } else { - for (const connectorStatus of chargingStation.connectors.values()) { - if ( - connectorStatus.reservation != null && - hasReservationExpired(connectorStatus.reservation) - ) { - reservations.push(connectorStatus.reservation) - } + for (const { connectorStatus } of chargingStation.iterateConnectors()) { + if (connectorStatus.reservation != null && hasReservationExpired(connectorStatus.reservation)) { + reservations.push(connectorStatus.reservation) } } const results = await Promise.allSettled( @@ -275,7 +249,7 @@ export const validateStationInfo = (chargingStation: ChargingStation): void => { switch (chargingStation.stationInfo.ocppVersion) { case OCPPVersion.VERSION_20: case OCPPVersion.VERSION_201: - if (isEmpty(chargingStation.evses)) { + if (chargingStation.getNumberOfEvses() === 0) { throw new BaseError( `${chargingStationId}: OCPP ${chargingStation.stationInfo.ocppVersion} requires at least one EVSE defined in the charging station template/configuration` ) diff --git a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts index 9b4844c6..33b802c2 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -499,29 +499,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { } ) .catch(errorHandler) - } else if (chargingStation.hasEvses) { - for (const evseStatus of chargingStation.evses.values()) { - for (const [id, connectorStatus] of evseStatus.connectors) { - chargingStation.ocppRequestService - .requestHandler< - OCPP16StatusNotificationRequest, - OCPP16StatusNotificationResponse - >( - chargingStation, - OCPP16RequestCommand.STATUS_NOTIFICATION, - { - connectorId: id, - status: connectorStatus.status as OCPP16ChargePointStatus, - } as unknown as OCPP16StatusNotificationRequest, - { - triggerMessage: true, - } - ) - .catch(errorHandler) - } - } } else { - for (const [id, connectorStatus] of chargingStation.connectors) { + for (const { connectorId, connectorStatus } of chargingStation.iterateConnectors()) { chargingStation.ocppRequestService .requestHandler< OCPP16StatusNotificationRequest, @@ -530,7 +509,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { chargingStation, OCPP16RequestCommand.STATUS_NOTIFICATION, { - connectorId: id, + connectorId, status: connectorStatus.status as OCPP16ChargePointStatus, } as unknown as OCPP16StatusNotificationRequest, { @@ -716,25 +695,16 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { ? OCPP16ChargePointStatus.Available : OCPP16ChargePointStatus.Unavailable if (connectorId === 0) { - let response: OCPP16ChangeAvailabilityResponse | undefined - if (chargingStation.hasEvses) { - for (const evseStatus of chargingStation.evses.values()) { - response = await OCPP16ServiceUtils.changeAvailability( - chargingStation, - [...evseStatus.connectors.keys()], - chargePointStatus, - type - ) - } - } else { - response = await OCPP16ServiceUtils.changeAvailability( - chargingStation, - [...chargingStation.connectors.keys()], - chargePointStatus, - type - ) - } - return response ?? OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED + const response = await OCPP16ServiceUtils.changeAvailability( + chargingStation, + chargingStation + .iterateConnectors() + .map(({ connectorId }) => connectorId) + .toArray(), + chargePointStatus, + type + ) + return response } else if ( connectorId > 0 && (chargingStation.isChargingStationAvailable() || @@ -908,29 +878,14 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { } } else { let clearedCP = false - if (chargingStation.hasEvses) { - for (const evseStatus of chargingStation.evses.values()) { - for (const status of evseStatus.connectors.values()) { - const clearedConnectorCP = OCPP16ServiceUtils.clearChargingProfiles( - chargingStation, - commandPayload, - status.chargingProfiles as OCPP16ChargingProfile[] - ) - if (clearedConnectorCP && !clearedCP) { - clearedCP = true - } - } - } - } else { - for (const id of chargingStation.connectors.keys()) { - const clearedConnectorCP = OCPP16ServiceUtils.clearChargingProfiles( - chargingStation, - commandPayload, - chargingStation.getConnectorStatus(id)?.chargingProfiles as OCPP16ChargingProfile[] - ) - if (clearedConnectorCP && !clearedCP) { - clearedCP = true - } + for (const { connectorStatus } of chargingStation.iterateConnectors()) { + const clearedConnectorCP = OCPP16ServiceUtils.clearChargingProfiles( + chargingStation, + commandPayload, + connectorStatus.chargingProfiles as OCPP16ChargingProfile[] + ) + if (clearedConnectorCP && !clearedCP) { + clearedCP = true } } if (clearedCP) { @@ -1683,30 +1638,12 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { if (!checkChargingStationState(chargingStation, chargingStation.logPrefix())) { return } - if (chargingStation.hasEvses) { - for (const [evseId, evseStatus] of chargingStation.evses) { - if (evseId > 0) { - for (const [connectorId, connectorStatus] of evseStatus.connectors) { - if (connectorStatus.transactionStarted === false) { - await OCPP16ServiceUtils.sendAndSetConnectorStatus(chargingStation, { - connectorId, - status: OCPP16ChargePointStatus.Unavailable, - } as OCPP16StatusNotificationRequest) - } - } - } - } - } else { - for (const connectorId of chargingStation.connectors.keys()) { - if ( - connectorId > 0 && - chargingStation.getConnectorStatus(connectorId)?.transactionStarted === false - ) { - await OCPP16ServiceUtils.sendAndSetConnectorStatus(chargingStation, { - connectorId, - status: OCPP16ChargePointStatus.Unavailable, - } as OCPP16StatusNotificationRequest) - } + for (const { connectorId, connectorStatus } of chargingStation.iterateConnectors(true)) { + if (connectorStatus.transactionStarted === false) { + await OCPP16ServiceUtils.sendAndSetConnectorStatus(chargingStation, { + connectorId, + status: OCPP16ChargePointStatus.Unavailable, + } as OCPP16StatusNotificationRequest) } } await chargingStation.ocppRequestService.requestHandler< @@ -1758,31 +1695,12 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { transactionsStarted = true wasTransactionsStarted = true } else { - if (chargingStation.hasEvses) { - for (const [evseId, evseStatus] of chargingStation.evses) { - if (evseId > 0) { - for (const [connectorId, connectorStatus] of evseStatus.connectors) { - if (connectorStatus.status !== OCPP16ChargePointStatus.Unavailable) { - await OCPP16ServiceUtils.sendAndSetConnectorStatus(chargingStation, { - connectorId, - status: OCPP16ChargePointStatus.Unavailable, - } as OCPP16StatusNotificationRequest) - } - } - } - } - } else { - for (const connectorId of chargingStation.connectors.keys()) { - if ( - connectorId > 0 && - chargingStation.getConnectorStatus(connectorId)?.status !== - OCPP16ChargePointStatus.Unavailable - ) { - await OCPP16ServiceUtils.sendAndSetConnectorStatus(chargingStation, { - connectorId, - status: OCPP16ChargePointStatus.Unavailable, - } as OCPP16StatusNotificationRequest) - } + for (const { connectorId, connectorStatus } of chargingStation.iterateConnectors(true)) { + if (connectorStatus.status !== OCPP16ChargePointStatus.Unavailable) { + await OCPP16ServiceUtils.sendAndSetConnectorStatus(chargingStation, { + connectorId, + status: OCPP16ChargePointStatus.Unavailable, + } as OCPP16StatusNotificationRequest) } } transactionsStarted = false diff --git a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts index c6b383c8..85e1e7e8 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts @@ -150,26 +150,10 @@ export class OCPP16ResponseService extends OCPPResponseService { requestPayload: OCPP16AuthorizeRequest ): void { let authorizeConnectorId: number | undefined - if (chargingStation.hasEvses) { - for (const [evseId, evseStatus] of chargingStation.evses) { - if (evseId > 0) { - for (const [connectorId, connectorStatus] of evseStatus.connectors) { - if (connectorStatus.authorizeIdTag === requestPayload.idTag) { - authorizeConnectorId = connectorId - break - } - } - } - } - } else { - for (const connectorId of chargingStation.connectors.keys()) { - if ( - connectorId > 0 && - chargingStation.getConnectorStatus(connectorId)?.authorizeIdTag === requestPayload.idTag - ) { - authorizeConnectorId = connectorId - break - } + for (const { connectorId, connectorStatus } of chargingStation.iterateConnectors(true)) { + if (connectorStatus.authorizeIdTag === requestPayload.idTag) { + authorizeConnectorId = connectorId + break } } if (authorizeConnectorId != null) { @@ -344,7 +328,7 @@ export class OCPP16ResponseService extends OCPPResponseService { return } if (chargingStation.hasEvses) { - for (const [evseId, evseStatus] of chargingStation.evses) { + for (const { evseId, evseStatus } of chargingStation.iterateEvses()) { if (evseStatus.connectors.size > 1) { for (const [id, status] of evseStatus.connectors) { if (id !== connectorId && status.transactionStarted === true) { diff --git a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts index c5376875..ecf553d5 100644 --- a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts @@ -10,6 +10,7 @@ import { CertificateSigningUseEnumType, ChangeAvailabilityStatusEnumType, ConnectorEnumType, + type ConnectorStatus, ConnectorStatusEnum, CustomerInformationStatusEnumType, DataEnumType, @@ -416,7 +417,10 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { OCPP20TriggerReasonEnumType.RemoteStart, connectorId, response.transactionId, - startedMeterValues.length > 0 ? { meterValue: startedMeterValues } : undefined + { + ...(startedMeterValues.length > 0 && { meterValue: startedMeterValues }), + remoteStartId: request.remoteStartId, + } ).catch((error: unknown) => { logger.error( `${chargingStation.logPrefix()} ${moduleName}.constructor: TransactionEvent(Started) error:`, @@ -510,10 +514,8 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { if (evse?.id != null && evse.id > 0) { targetEvseIds.push(evse.id) } else { - for (const [eId] of chargingStation.evses) { - if (eId > 0) { - targetEvseIds.push(eId) - } + for (const { evseId } of chargingStation.iterateEvses(true)) { + targetEvseIds.push(evseId) } } let hasSentTransactionEvent = false @@ -918,61 +920,44 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } if (chargingStation.hasEvses) { - for (const [evseId, evse] of chargingStation.evses) { + for (const { evseId, evseStatus } of chargingStation.iterateEvses()) { reportData.push({ component: { evse: { id: evseId }, name: OCPP20ComponentName.EVSE, }, variable: { name: OCPP20DeviceInfoVariableName.AvailabilityState }, - variableAttribute: [{ type: AttributeEnumType.Actual, value: evse.availability }], + variableAttribute: [ + { type: AttributeEnumType.Actual, value: evseStatus.availability }, + ], variableCharacteristics: { dataType: DataEnumType.string, supportsMonitoring: true }, }) - if (evse.connectors.size > 0) { - for (const [connectorId, connector] of evse.connectors) { - reportData.push({ - component: { - evse: { connectorId, id: evseId }, - name: OCPP20ComponentName.EVSE, - }, - variable: { name: OCPP20DeviceInfoVariableName.ConnectorType }, - variableAttribute: [ - { - type: AttributeEnumType.Actual, - value: connector.type ?? ConnectorEnumType.Unknown, - }, - ], - variableCharacteristics: { - dataType: DataEnumType.string, - supportsMonitoring: false, - }, - }) - } - } - } - } else { - for (const [connectorId, connector] of chargingStation.connectors) { - if (connectorId > 0) { - reportData.push({ - component: { - evse: { connectorId, id: 1 }, - name: OCPP20ComponentName.Connector, - }, - variable: { name: OCPP20DeviceInfoVariableName.ConnectorType }, - variableAttribute: [ - { - type: AttributeEnumType.Actual, - value: connector.type ?? ConnectorEnumType.Unknown, - }, - ], - variableCharacteristics: { - dataType: DataEnumType.string, - supportsMonitoring: false, - }, - }) - } } } + for (const { + connectorId, + connectorStatus, + evseId, + } of chargingStation.iterateConnectors()) { + if (evseId == null && connectorId === 0) continue + reportData.push({ + component: { + evse: { connectorId, id: evseId ?? 1 }, + name: evseId != null ? OCPP20ComponentName.EVSE : OCPP20ComponentName.Connector, + }, + variable: { name: OCPP20DeviceInfoVariableName.ConnectorType }, + variableAttribute: [ + { + type: AttributeEnumType.Actual, + value: connectorStatus.type ?? ConnectorEnumType.Unknown, + }, + ], + variableCharacteristics: { + dataType: DataEnumType.string, + supportsMonitoring: false, + }, + }) + } break case ReportBaseEnumType.SummaryInventory: @@ -993,38 +978,38 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { }) if (chargingStation.hasEvses) { - for (const [evseId, evse] of chargingStation.evses) { + for (const { evseId, evseStatus } of chargingStation.iterateEvses()) { reportData.push({ component: { evse: { id: evseId }, name: OCPP20ComponentName.EVSE, }, variable: { name: OCPP20DeviceInfoVariableName.AvailabilityState }, - variableAttribute: [{ type: AttributeEnumType.Actual, value: evse.availability }], + variableAttribute: [ + { type: AttributeEnumType.Actual, value: evseStatus.availability }, + ], variableCharacteristics: { dataType: DataEnumType.string, supportsMonitoring: true }, }) } } else { - for (const [connectorId, connector] of chargingStation.connectors) { - if (connectorId > 0) { - reportData.push({ - component: { - evse: { connectorId, id: 1 }, - name: OCPP20ComponentName.Connector, - }, - variable: { name: OCPP20DeviceInfoVariableName.AvailabilityState }, - variableAttribute: [ - { - type: AttributeEnumType.Actual, - value: connector.status ?? ConnectorStatusEnum.Unavailable, - }, - ], - variableCharacteristics: { - dataType: DataEnumType.string, - supportsMonitoring: true, + for (const { connectorId, connectorStatus } of chargingStation.iterateConnectors(true)) { + reportData.push({ + component: { + evse: { connectorId, id: 1 }, + name: OCPP20ComponentName.Connector, + }, + variable: { name: OCPP20DeviceInfoVariableName.AvailabilityState }, + variableAttribute: [ + { + type: AttributeEnumType.Actual, + value: connectorStatus.status ?? ConnectorStatusEnum.Unavailable, }, - }) - } + ], + variableCharacteristics: { + dataType: DataEnumType.string, + supportsMonitoring: true, + }, + }) } } break @@ -1047,6 +1032,20 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } } + private connectorHasQueuedEvents ( + connectorStatus: ConnectorStatus, + transactionId?: string + ): boolean { + const queue = connectorStatus.transactionEventQueue + if (queue == null || queue.length === 0) { + return false + } + if (transactionId == null) { + return true + } + return queue.some(({ request }) => request.transactionInfo.transactionId === transactionId) + } + private getRestoredConnectorStatus ( chargingStation: ChargingStation, connectorId: number @@ -1079,7 +1078,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { operationalStatus: OCPP20OperationalStatusEnumType, newConnectorStatus: OCPP20ConnectorStatusEnumType ): OCPP20ChangeAvailabilityResponse { - if (!chargingStation.evses.has(evseId)) { + if (!chargingStation.hasEvse(evseId)) { return { status: ChangeAvailabilityStatusEnumType.Rejected, statusInfo: { @@ -1129,10 +1128,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { newConnectorStatus: OCPP20ConnectorStatusEnumType ): OCPP20ChangeAvailabilityResponse | undefined { let hasActiveTransactions = false - for (const [evseId, evseStatus] of chargingStation.evses) { - if (evseId === 0) { - continue - } + for (const { evseId, evseStatus } of chargingStation.iterateEvses(true)) { if (this.hasEvseActiveTransactions(evseStatus)) { hasActiveTransactions = true logger.info( @@ -1146,8 +1142,8 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } } if (hasActiveTransactions) { - for (const [evseId, evseStatus] of chargingStation.evses) { - if (evseId > 0 && !this.hasEvseActiveTransactions(evseStatus)) { + for (const { evseId, evseStatus } of chargingStation.iterateEvses(true)) { + if (!this.hasEvseActiveTransactions(evseStatus)) { this.sendEvseStatusNotifications(chargingStation, evseId, newConnectorStatus) } } @@ -1167,7 +1163,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { operationalStatus: OCPP20OperationalStatusEnumType, newConnectorStatus: OCPP20ConnectorStatusEnumType ): OCPP20ChangeAvailabilityResponse { - if (!chargingStation.evses.has(evseId)) { + if (!chargingStation.hasEvse(evseId)) { logger.warn( `${chargingStation.logPrefix()} ${moduleName}.handleRequestChangeAvailability: EVSE ${evseId.toString()} not found, rejecting` ) @@ -1421,10 +1417,8 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } // Apply availability change to all EVSEs (for Operative, or Inoperative with no active transactions) - for (const [evseId, evseStatus] of chargingStation.evses) { - if (evseId > 0) { - evseStatus.availability = operationalStatus - } + for (const { evseStatus } of chargingStation.iterateEvses(true)) { + evseStatus.availability = operationalStatus } if (operationalStatus === OCPP20OperationalStatusEnumType.Operative) { this.sendRestoredAllConnectorsStatusNotifications(chargingStation) @@ -1775,16 +1769,14 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { // E14.FR.06: When transactionId is omitted, ongoingIndicator SHALL NOT be set if (transactionId == null) { return { - // Simulator has no persistent offline message buffer - messagesInQueue: false, + messagesInQueue: this.hasQueuedTransactionEvents(chargingStation), } } const evseId = chargingStation.getEvseIdByTransactionId(transactionId) return { - // Simulator has no persistent offline message buffer - messagesInQueue: false, + messagesInQueue: this.hasQueuedTransactionEvents(chargingStation, transactionId), ongoingIndicator: evseId != null, } } @@ -1946,7 +1938,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } } - const evseExists = chargingStation.evses.has(evseId) + const evseExists = chargingStation.hasEvse(evseId) if (!evseExists) { logger.warn( `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: EVSE ${evseId.toString()} not found, rejecting reset request` @@ -2676,7 +2668,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } } - if (!chargingStation.evses.has(evseId)) { + if (!chargingStation.hasEvse(evseId)) { return { status: UnlockStatusEnumType.UnknownConnector, statusInfo: { @@ -2784,9 +2776,9 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } } - const hasActiveTransactions = [...chargingStation.evses].some( - ([evseId, evse]) => evseId > 0 && this.hasEvseActiveTransactions(evse) - ) + const hasActiveTransactions = chargingStation + .iterateEvses(true) + .some(({ evseStatus }) => this.hasEvseActiveTransactions(evseStatus)) if (hasActiveTransactions) { logger.info( `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Active transactions detected — installation will be deferred until idle` @@ -2860,6 +2852,18 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { ) } + private hasQueuedTransactionEvents ( + chargingStation: ChargingStation, + transactionId?: string + ): boolean { + for (const { connectorStatus } of chargingStation.iterateConnectors()) { + if (this.connectorHasQueuedEvents(connectorStatus, transactionId)) { + return true + } + } + return false + } + private isAuthorizedToStopTransaction ( chargingStation: ChargingStation, connectorId: number, @@ -3061,7 +3065,10 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { const evseIds = evseId != null && evseId > 0 ? [evseId] - : [...chargingStation.evses.keys()].filter(id => id > 0) + : chargingStation + .iterateEvses(true) + .map(({ evseId }) => evseId) + .toArray() for (const id of evseIds) { const evseStatus = chargingStation.getEvseStatus(id) if (evseStatus != null) { @@ -3167,10 +3174,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } private selectAvailableEvse (chargingStation: ChargingStation): number | undefined { - for (const [evseId, evseStatus] of chargingStation.evses) { - if (evseId === 0) { - continue - } + for (const { evseId, evseStatus } of chargingStation.iterateEvses(true)) { if ( evseStatus.availability !== OCPP20OperationalStatusEnumType.Inoperative && !this.hasEvseActiveTransactions(evseStatus) @@ -3185,18 +3189,16 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { chargingStation: ChargingStation, status: OCPP20ConnectorStatusEnumType ): void { - for (const [, evse] of chargingStation.evses) { - for (const [connectorId] of evse.connectors) { - sendAndSetConnectorStatus(chargingStation, { - connectorId, - connectorStatus: status, - } as unknown as OCPP20StatusNotificationRequest).catch((error: unknown) => { - logger.error( - `${chargingStation.logPrefix()} ${moduleName}.sendAllConnectorsStatusNotifications: Error sending status notification for connector ${connectorId.toString()}:`, - error - ) - }) - } + for (const { connectorId } of chargingStation.iterateConnectors()) { + sendAndSetConnectorStatus(chargingStation, { + connectorId, + connectorStatus: status, + } as unknown as OCPP20StatusNotificationRequest).catch((error: unknown) => { + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.sendAllConnectorsStatusNotifications: Error sending status notification for connector ${connectorId.toString()}:`, + error + ) + }) } } @@ -3347,21 +3349,17 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } private sendRestoredAllConnectorsStatusNotifications (chargingStation: ChargingStation): void { - for (const [evseId, evseStatus] of chargingStation.evses) { - if (evseId > 0) { - for (const [connectorId] of evseStatus.connectors) { - const restoredStatus = this.getRestoredConnectorStatus(chargingStation, connectorId) - sendAndSetConnectorStatus(chargingStation, { - connectorId, - connectorStatus: restoredStatus, - } as unknown as OCPP20StatusNotificationRequest).catch((error: unknown) => { - logger.error( - `${chargingStation.logPrefix()} ${moduleName}.sendRestoredAllConnectorsStatusNotifications: Error sending status notification for connector ${connectorId.toString()}:`, - error - ) - }) - } - } + for (const { connectorId } of chargingStation.iterateConnectors(true)) { + const restoredStatus = this.getRestoredConnectorStatus(chargingStation, connectorId) + sendAndSetConnectorStatus(chargingStation, { + connectorId, + connectorStatus: restoredStatus, + } as unknown as OCPP20StatusNotificationRequest).catch((error: unknown) => { + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.sendRestoredAllConnectorsStatusNotifications: Error sending status notification for connector ${connectorId.toString()}:`, + error + ) + }) } } @@ -3552,9 +3550,9 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { // L01.FR.06: Wait for active transactions to end before installing // L01.FR.07: Set idle connectors to Unavailable when AllowNewSessionsPendingFirmwareUpdate is false/absent - const hasActiveTransactionsBeforeInstall = [...chargingStation.evses].some( - ([evseId, evse]) => evseId > 0 && this.hasEvseActiveTransactions(evse) - ) + const hasActiveTransactionsBeforeInstall = chargingStation + .iterateEvses(true) + .some(({ evseStatus }) => this.hasEvseActiveTransactions(evseStatus)) if (hasActiveTransactionsBeforeInstall) { const variableManager = OCPP20VariableManager.getInstance() const allowNewSessionsResults = variableManager.getVariables(chargingStation, [ @@ -3567,17 +3565,17 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { const allowNewSessions = convertToBoolean(allowNewSessionsResults[0]?.attributeValue) while ( !checkAborted() && - [...chargingStation.evses].some( - ([evseId, evse]) => evseId > 0 && this.hasEvseActiveTransactions(evse) - ) + chargingStation + .iterateEvses(true) + .some(({ evseStatus }) => this.hasEvseActiveTransactions(evseStatus)) ) { // L01.FR.07: Set newly-available EVSE to Unavailable on each iteration if (!allowNewSessions) { - for (const [fwEvseId, fwEvseStatus] of chargingStation.evses) { - if (fwEvseId > 0 && !this.hasEvseActiveTransactions(fwEvseStatus)) { + for (const { evseId, evseStatus } of chargingStation.iterateEvses(true)) { + if (!this.hasEvseActiveTransactions(evseStatus)) { this.sendEvseStatusNotifications( chargingStation, - fwEvseId, + evseId, OCPP20ConnectorStatusEnumType.Unavailable ) } @@ -3685,18 +3683,16 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { chargingStation: ChargingStation, errorHandler: (error: unknown) => void ): void { - for (const [evseId, evseStatus] of chargingStation.evses) { - if (evseId > 0) { - for (const [connectorId, connectorStatus] of evseStatus.connectors) { - const resolvedStatus = connectorStatus.status ?? ConnectorStatusEnum.Available - chargingStation.ocppRequestService - .requestHandler< - OCPP20StatusNotificationRequest, - OCPP20StatusNotificationResponse - >(chargingStation, OCPP20RequestCommand.STATUS_NOTIFICATION, { connectorId, connectorStatus: resolvedStatus, evseId } as unknown as OCPP20StatusNotificationRequest, { skipBufferingOnError: true, triggerMessage: true }) - .catch(errorHandler) - } - } + for (const { connectorId, connectorStatus, evseId } of chargingStation.iterateConnectors( + true + )) { + const resolvedStatus = connectorStatus.status ?? ConnectorStatusEnum.Available + chargingStation.ocppRequestService + .requestHandler< + OCPP20StatusNotificationRequest, + OCPP20StatusNotificationResponse + >(chargingStation, OCPP20RequestCommand.STATUS_NOTIFICATION, { connectorId, connectorStatus: resolvedStatus, evseId } as unknown as OCPP20StatusNotificationRequest, { skipBufferingOnError: true, triggerMessage: true }) + .catch(errorHandler) } } @@ -3706,7 +3702,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { errorHandler: (error: unknown) => void ): void { if (evse?.id !== undefined && evse.id > 0 && evse.connectorId !== undefined) { - const evseStatus = chargingStation.evses.get(evse.id) + const evseStatus = chargingStation.getEvseStatus(evse.id) const connectorStatus = evseStatus?.connectors.get(evse.connectorId) const resolvedStatus = connectorStatus?.status ?? ConnectorStatusEnum.Available chargingStation.ocppRequestService @@ -4040,7 +4036,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { }, } } - if (!chargingStation.evses.has(evse.id)) { + if (!chargingStation.hasEvse(evse.id)) { return { status: TriggerMessageStatusEnumType.Rejected, statusInfo: { diff --git a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts index f56c25ef..015bfe15 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts @@ -742,27 +742,26 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { } } } else { - for (const [iteratedEvseId, evseStatus] of chargingStation.evses) { - if (iteratedEvseId === 0) { - continue - } - for (const [connectorId, connectorStatus] of evseStatus.connectors) { - if (connectorStatus.transactionId != null) { - terminationPromises.push( - OCPP20ServiceUtils.requestStopTransaction( - chargingStation, - connectorId, - iteratedEvseId, - triggerReason, - stoppedReason - ).catch((error: unknown) => { - logger.error( - `${chargingStation.logPrefix()} ${moduleName}.stopAllTransactions: Error stopping transaction on connector ${connectorId.toString()}:`, - error - ) - }) - ) - } + for (const { + connectorId, + connectorStatus, + evseId: connectorEvseId, + } of chargingStation.iterateConnectors(true)) { + if (connectorStatus.transactionId != null) { + terminationPromises.push( + OCPP20ServiceUtils.requestStopTransaction( + chargingStation, + connectorId, + connectorEvseId, + triggerReason, + stoppedReason + ).catch((error: unknown) => { + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.stopAllTransactions: Error stopping transaction on connector ${connectorId.toString()}:`, + error + ) + }) + ) } } } diff --git a/src/charging-station/ocpp/OCPPServiceUtils.ts b/src/charging-station/ocpp/OCPPServiceUtils.ts index 414f5e8f..cd95c258 100644 --- a/src/charging-station/ocpp/OCPPServiceUtils.ts +++ b/src/charging-station/ocpp/OCPPServiceUtils.ts @@ -522,33 +522,9 @@ export const stopRunningTransactions = async ( case OCPPVersion.VERSION_16: { const { OCPP16ServiceUtils } = await import('./1.6/OCPP16ServiceUtils.js') // Sequential — OCPP 1.6 behavior - if (chargingStation.hasEvses) { - for (const [evseId, evseStatus] of chargingStation.evses) { - if (evseId === 0) { - continue - } - for (const [connectorId, connectorStatus] of evseStatus.connectors) { - if (connectorStatus.transactionStarted === true) { - await OCPP16ServiceUtils.stopTransactionOnConnector( - chargingStation, - connectorId, - reason - ) - } - } - } - } else { - for (const connectorId of chargingStation.connectors.keys()) { - if ( - connectorId > 0 && - chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true - ) { - await OCPP16ServiceUtils.stopTransactionOnConnector( - chargingStation, - connectorId, - reason - ) - } + for (const { connectorId, connectorStatus } of chargingStation.iterateConnectors(true)) { + if (connectorStatus.transactionStarted === true) { + await OCPP16ServiceUtils.stopTransactionOnConnector(chargingStation, connectorId, reason) } } break @@ -623,35 +599,16 @@ export const flushQueuedTransactionMessages = async ( case OCPPVersion.VERSION_20: case OCPPVersion.VERSION_201: { const { OCPP20ServiceUtils } = await import('./2.0/OCPP20ServiceUtils.js') - if (chargingStation.hasEvses) { - for (const evseStatus of chargingStation.evses.values()) { - for (const [connectorId, connectorStatus] of evseStatus.connectors) { - if ((connectorStatus.transactionEventQueue?.length ?? 0) > 0) { - await OCPP20ServiceUtils.sendQueuedTransactionEvents( - chargingStation, - connectorId - ).catch((error: unknown) => { - logger.error( - `${chargingStation.logPrefix()} OCPPServiceUtils.flushQueuedTransactionMessages: Error flushing queued TransactionEvents:`, - error - ) - }) - } - } - } - } else { - for (const [connectorId, connectorStatus] of chargingStation.connectors) { - if ((connectorStatus.transactionEventQueue?.length ?? 0) > 0) { - await OCPP20ServiceUtils.sendQueuedTransactionEvents( - chargingStation, - connectorId - ).catch((error: unknown) => { + for (const { connectorId, connectorStatus } of chargingStation.iterateConnectors()) { + if ((connectorStatus.transactionEventQueue?.length ?? 0) > 0) { + await OCPP20ServiceUtils.sendQueuedTransactionEvents(chargingStation, connectorId).catch( + (error: unknown) => { logger.error( `${chargingStation.logPrefix()} OCPPServiceUtils.flushQueuedTransactionMessages: Error flushing queued TransactionEvents:`, error ) - }) - } + } + ) } } break diff --git a/src/types/ChargingStationWorker.ts b/src/types/ChargingStationWorker.ts index 4a4806b7..551c1f70 100644 --- a/src/types/ChargingStationWorker.ts +++ b/src/types/ChargingStationWorker.ts @@ -7,9 +7,9 @@ import type { } from './AutomaticTransactionGenerator.js' import type { ChargingStationInfo } from './ChargingStationInfo.js' import type { ChargingStationOcppConfiguration } from './ChargingStationOcppConfiguration.js' -import type { ConnectorStatus } from './ConnectorStatus.js' +import type { ConnectorEntry } from './ConnectorStatus.js' +import type { EvseEntry } from './Evse.js' import type { JsonObject } from './JsonType.js' -import type { AvailabilityType } from './ocpp/Requests.js' import type { BootNotificationResponse } from './ocpp/Responses.js' import type { Statistics } from './Statistics.js' import type { UUIDv4 } from './UUID.js' @@ -79,14 +79,3 @@ export const ChargingStationWorkerMessageEvents = { export type ChargingStationWorkerMessageEvents = | ChargingStationEvents | ChargingStationMessageEvents - -export interface ConnectorEntry { - connector: ConnectorStatus - connectorId: number -} - -export interface EvseEntry { - availability: AvailabilityType - connectors: ConnectorEntry[] - evseId: number -} diff --git a/src/types/ConnectorStatus.ts b/src/types/ConnectorStatus.ts index 56fb4913..ec846b36 100644 --- a/src/types/ConnectorStatus.ts +++ b/src/types/ConnectorStatus.ts @@ -7,6 +7,12 @@ import type { MeterValue } from './ocpp/MeterValues.js' import type { AvailabilityType } from './ocpp/Requests.js' import type { Reservation } from './ocpp/Reservation.js' +export interface ConnectorEntry { + readonly connectorId: number + readonly connectorStatus: ConnectorStatus + readonly evseId: number | undefined +} + export interface ConnectorStatus { authorizeIdTag?: string availability: AvailabilityType diff --git a/src/types/Evse.ts b/src/types/Evse.ts index e1e36b02..1421a20b 100644 --- a/src/types/Evse.ts +++ b/src/types/Evse.ts @@ -2,6 +2,11 @@ import type { ConnectorStatus } from './ConnectorStatus.js' import type { SampledValueTemplate } from './MeasurandPerPhaseSampledValueTemplates.js' import type { AvailabilityType } from './ocpp/Requests.js' +export interface EvseEntry { + readonly evseId: number + readonly evseStatus: EvseStatus +} + export interface EvseStatus { availability: AvailabilityType connectors: Map diff --git a/src/types/index.ts b/src/types/index.ts index 2fb0f64e..aa893493 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -32,8 +32,6 @@ export { type ChargingStationWorkerMessage, type ChargingStationWorkerMessageData, ChargingStationWorkerMessageEvents, - type ConnectorEntry, - type EvseEntry, } from './ChargingStationWorker.js' export { ApplicationProtocolVersion, @@ -47,10 +45,10 @@ export { type UIServerConfiguration, type WorkerConfiguration, } from './ConfigurationData.js' -export type { ConnectorStatus } from './ConnectorStatus.js' +export type { ConnectorEntry, ConnectorStatus } from './ConnectorStatus.js' export type { EmptyObject } from './EmptyObject.js' export type { HandleErrorParams } from './Error.js' -export type { EvseStatus, EvseTemplate } from './Evse.js' +export type { EvseEntry, EvseStatus, EvseTemplate } from './Evse.js' export { FileType } from './FileType.js' export type { JsonObject, JsonType } from './JsonType.js' export { MapStringifyFormat } from './MapStringifyFormat.js' diff --git a/src/utils/ChargingStationConfigurationUtils.ts b/src/utils/ChargingStationConfigurationUtils.ts index a21e28a3..e9fd082a 100644 --- a/src/utils/ChargingStationConfigurationUtils.ts +++ b/src/utils/ChargingStationConfigurationUtils.ts @@ -31,85 +31,105 @@ export const buildChargingStationAutomaticTransactionGeneratorConfiguration = ( } export const buildConnectorEntries = (chargingStation: ChargingStation): ConnectorEntry[] => { - return [...chargingStation.connectors.entries()].map( - ([ - connectorId, - { - transactionEndedMeterValues, - transactionEndedMeterValuesSetInterval, - transactionEventQueue, - transactionUpdatedMeterValuesSetInterval, - ...connector - }, - ]) => ({ - connector, - connectorId, - }) - ) -} - -export const buildConnectorsStatus = ( - chargingStation: ChargingStation -): [number, ConnectorStatus][] => { - return [...chargingStation.connectors.entries()].map( - ([ - connectorId, - { - transactionEndedMeterValues, - transactionEndedMeterValuesSetInterval, - transactionEventQueue, - transactionUpdatedMeterValuesSetInterval, - ...connectorStatus - }, - ]) => [connectorId, connectorStatus] - ) -} - -export const buildEvseEntries = (chargingStation: ChargingStation): EvseEntry[] => { - return [...chargingStation.evses.entries()].map(([evseId, evseStatus]) => ({ - availability: evseStatus.availability, - connectors: [...evseStatus.connectors.entries()].map( - ([ + if (chargingStation.hasEvses) { + return [] + } + return chargingStation + .iterateConnectors() + .map( + ({ connectorId, - { + connectorStatus: { transactionEndedMeterValues, transactionEndedMeterValuesSetInterval, transactionEventQueue, transactionUpdatedMeterValuesSetInterval, - ...connector + ...connectorStatus }, - ]) => ({ - connector, + }) => ({ connectorId, + connectorStatus, + evseId: undefined, }) - ), - evseId, - })) + ) + .toArray() } -export const buildEvsesStatus = ( +export const buildConnectorsStatus = ( chargingStation: ChargingStation -): [number, EvseStatusConfiguration][] => { - return [...chargingStation.evses.entries()].map(([evseId, evseStatus]) => { - const connectorsStatus: [number, ConnectorStatus][] = [...evseStatus.connectors.entries()].map( - ([ +): [number, ConnectorStatus][] => { + if (chargingStation.hasEvses) { + return [] + } + return chargingStation + .iterateConnectors() + .map( + ({ connectorId, - { + connectorStatus: { transactionEndedMeterValues, transactionEndedMeterValuesSetInterval, transactionEventQueue, transactionUpdatedMeterValuesSetInterval, - ...connector + ...connectorStatus }, - ]) => [connectorId, connector] + }) => [connectorId, connectorStatus] as [number, ConnectorStatus] ) - const { connectors: _, ...evseStatusRest } = evseStatus - return [ + .toArray() +} + +export const buildEvseEntries = (chargingStation: ChargingStation): EvseEntry[] => { + return chargingStation + .iterateEvses() + .map(({ evseId, evseStatus }) => ({ evseId, - { - ...evseStatusRest, - connectorsStatus, - } as EvseStatusConfiguration, - ] - }) + evseStatus: { + availability: evseStatus.availability, + connectors: [...evseStatus.connectors.entries()].map( + ([ + connectorId, + { + transactionEndedMeterValues, + transactionEndedMeterValuesSetInterval, + transactionEventQueue, + transactionUpdatedMeterValuesSetInterval, + ...connectorStatus + }, + ]) => ({ connectorId, connectorStatus, evseId }) + ), + }, + })) + .toArray() as unknown as EvseEntry[] +} + +export const buildEvsesStatus = ( + chargingStation: ChargingStation +): [number, EvseStatusConfiguration][] => { + return chargingStation + .iterateEvses() + .map(({ evseId, evseStatus }) => { + const connectorsStatus: [number, ConnectorStatus][] = [ + ...evseStatus.connectors.entries(), + ].map( + ([ + connectorId, + { + transactionEndedMeterValues, + transactionEndedMeterValuesSetInterval, + transactionEventQueue, + transactionUpdatedMeterValuesSetInterval, + ...connector + }, + ]) => [connectorId, connector] + ) + const { connectors: _, ...evseStatusRest } = evseStatus + return [ + evseId, + { + ...evseStatusRest, + connectorsStatus, + } as EvseStatusConfiguration, + ] as [number, EvseStatusConfiguration] + }) + .toArray() } diff --git a/tests/charging-station/ChargingStation-Lifecycle.test.ts b/tests/charging-station/ChargingStation-Lifecycle.test.ts index 589fa193..076f68ff 100644 --- a/tests/charging-station/ChargingStation-Lifecycle.test.ts +++ b/tests/charging-station/ChargingStation-Lifecycle.test.ts @@ -186,8 +186,8 @@ await describe('ChargingStation Lifecycle', async () => { await station.delete(false) // Assert - connectors and evses should be cleared - assert.strictEqual(station.connectors.size, 0) - assert.strictEqual(station.evses.size, 0) + assert.strictEqual(station.getNumberOfConnectors(), 0) + assert.strictEqual(station.getNumberOfEvses(), 0) assert.strictEqual(station.requests.size, 0) }) @@ -203,7 +203,7 @@ await describe('ChargingStation Lifecycle', async () => { // Assert - station should be stopped and cleared assert.strictEqual(station.started, false) - assert.strictEqual(station.connectors.size, 0) + assert.strictEqual(station.getNumberOfConnectors(), 0) }) await it('should handle delete operation with pending transactions', async () => { @@ -227,8 +227,8 @@ await describe('ChargingStation Lifecycle', async () => { // Assert - Station should be stopped and resources cleared assert.strictEqual(station.started, false) - assert.strictEqual(station.connectors.size, 0) - assert.strictEqual(station.evses.size, 0) + assert.strictEqual(station.getNumberOfConnectors(), 0) + assert.strictEqual(station.getNumberOfEvses(), 0) }) }) }) diff --git a/tests/charging-station/ChargingStation-Resilience.test.ts b/tests/charging-station/ChargingStation-Resilience.test.ts index 40a58ceb..a5929b79 100644 --- a/tests/charging-station/ChargingStation-Resilience.test.ts +++ b/tests/charging-station/ChargingStation-Resilience.test.ts @@ -118,7 +118,7 @@ await describe('ChargingStation Resilience', async () => { mocks.webSocket.simulateMessage('invalid json') // Assert - Station should still be operational (not crashed) - assert.strictEqual(station.connectors.size > 0, true) + assert.strictEqual(station.getNumberOfConnectors() > 0, true) }) await it('should handle WebSocket error event gracefully', () => { @@ -196,7 +196,7 @@ await describe('ChargingStation Resilience', async () => { mocks.webSocket.simulateClose(1006, 'Server unreachable') // Assert - Station should remain in valid state - assert.strictEqual(station.connectors.size > 0, true) + assert.strictEqual(station.getNumberOfConnectors() > 0, true) assert.strictEqual(mocks.webSocket.readyState, 3) // CLOSED }) diff --git a/tests/charging-station/ChargingStation.test.ts b/tests/charging-station/ChargingStation.test.ts index c07cae1e..c456c864 100644 --- a/tests/charging-station/ChargingStation.test.ts +++ b/tests/charging-station/ChargingStation.test.ts @@ -40,7 +40,7 @@ await describe('ChargingStation', async () => { const station = result.station assert.notStrictEqual(station, undefined) - assert.strictEqual(station.connectors.size > 0, true) + assert.strictEqual(station.getNumberOfConnectors() > 0, true) assert.notStrictEqual(station.stationInfo, undefined) cleanupChargingStation(station) @@ -50,8 +50,8 @@ await describe('ChargingStation', async () => { const result = createMockChargingStation({ connectorsCount: 5 }) const station = result.station - // 5 connectors + connector 0 = 6 total - assert.strictEqual(station.connectors.size, 6) + // 5 connectors (excluding connector 0) + assert.strictEqual(station.getNumberOfConnectors(), 5) assert.strictEqual(station.hasConnector(5), true) cleanupChargingStation(station) diff --git a/tests/charging-station/Helpers.test.ts b/tests/charging-station/Helpers.test.ts index a064f3bb..bd3ede27 100644 --- a/tests/charging-station/Helpers.test.ts +++ b/tests/charging-station/Helpers.test.ts @@ -724,7 +724,7 @@ await describe('Helpers', async () => { baseName: TEST_CHARGING_STATION_BASE_NAME, connectorsCount: 2, }) - const connectorStatus = chargingStation.connectors.get(1) + const connectorStatus = chargingStation.getConnectorStatus(1) if (connectorStatus != null) { connectorStatus.reservation = createTestReservation(false) } @@ -752,7 +752,7 @@ await describe('Helpers', async () => { connectorsCount: 2, stationInfo: { ocppVersion: OCPPVersion.VERSION_201 }, }) - const firstEvse = chargingStation.evses.get(1) + const firstEvse = chargingStation.getEvseStatus(1) const firstConnector = firstEvse?.connectors.values().next().value if (firstConnector != null) { firstConnector.reservation = createTestReservation(false) @@ -769,7 +769,7 @@ await describe('Helpers', async () => { connectorsCount: 2, stationInfo: { ocppVersion: OCPPVersion.VERSION_201 }, }) - const firstEvse = chargingStation.evses.get(1) + const firstEvse = chargingStation.getEvseStatus(1) const firstConnector = firstEvse?.connectors.values().next().value if (firstConnector != null) { firstConnector.reservation = createTestReservation(true) diff --git a/tests/charging-station/helpers/StationHelpers.ts b/tests/charging-station/helpers/StationHelpers.ts index d4fb9455..c0d5813a 100644 --- a/tests/charging-station/helpers/StationHelpers.ts +++ b/tests/charging-station/helpers/StationHelpers.ts @@ -9,7 +9,9 @@ import type { ChargingStationInfo, ChargingStationOcppConfiguration, ChargingStationTemplate, + ConnectorEntry, ConnectorStatus, + EvseEntry, EvseStatus, Reservation, StopTransactionReason, @@ -217,7 +219,7 @@ export function cleanupChargingStation (station: ChargingStation): void { } // Clear connector transaction state and timers - for (const connectorStatus of station.connectors.values()) { + for (const { connectorStatus } of station.iterateConnectors()) { if (connectorStatus.transactionUpdatedMeterValuesSetInterval != null) { clearInterval(connectorStatus.transactionUpdatedMeterValuesSetInterval) connectorStatus.transactionUpdatedMeterValuesSetInterval = undefined @@ -229,7 +231,7 @@ export function cleanupChargingStation (station: ChargingStation): void { } // Clear EVSE connector transaction state and timers - for (const evseStatus of station.evses.values()) { + for (const { evseStatus } of station.iterateEvses()) { for (const connectorStatus of evseStatus.connectors.values()) { if (connectorStatus.transactionUpdatedMeterValuesSetInterval != null) { clearInterval(connectorStatus.transactionUpdatedMeterValuesSetInterval) @@ -303,7 +305,7 @@ export function createConnectorStatus ( * @example * ```typescript * const { station, mocks } = createMockChargingStation({ connectorsCount: 2 }) - * expect(station.connectors.size).toBe(3) // 0 + 2 connectors + * station.getNumberOfConnectors() // 2 (excludes connector 0) * station.wsConnection = mocks.webSocket * mocks.webSocket.simulateMessage('["3","uuid",{}]') * ``` @@ -644,6 +646,10 @@ export function createMockChargingStation ( return connectors.has(connectorId) }, + hasEvse (evseId: number): boolean { + return evses.has(evseId) + }, + // Getters get hasEvses (): boolean { return useEvses @@ -694,6 +700,30 @@ export function createMockChargingStation ( return this.wsConnection?.readyState === WebSocketReadyState.OPEN }, + * iterateConnectors (skipZero = false): Generator { + if (useEvses) { + for (const [evseId, evseStatus] of evses) { + if (skipZero && evseId === 0) continue + for (const [connectorId, connectorStatus] of evseStatus.connectors) { + if (skipZero && connectorId === 0) continue + yield { connectorId, connectorStatus, evseId } + } + } + } else { + for (const [connectorId, connectorStatus] of connectors) { + if (skipZero && connectorId === 0) continue + yield { connectorId, connectorStatus, evseId: undefined } + } + } + }, + + * iterateEvses (skipZero = false): Generator { + for (const [evseId, evseStatus] of evses) { + if (skipZero && evseId === 0) continue + yield { evseId, evseStatus } + } + }, + listenerCount: () => 0, lockConnector (connectorId: number): void { @@ -923,12 +953,12 @@ export function resetChargingStationState (station: ChargingStation): void { } // Reset connector statuses - for (const [connectorId, connectorStatus] of station.connectors) { + for (const { connectorId, connectorStatus } of station.iterateConnectors()) { resetConnectorStatus(connectorStatus, connectorId === 0) } // Reset EVSE connector statuses - for (const evseStatus of station.evses.values()) { + for (const { evseStatus } of station.iterateEvses()) { evseStatus.availability = AvailabilityType.Operative for (const connectorStatus of evseStatus.connectors.values()) { resetConnectorStatus(connectorStatus, false) diff --git a/tests/charging-station/ocpp/1.6/OCPP16IncomingRequestService-SmartCharging.test.ts b/tests/charging-station/ocpp/1.6/OCPP16IncomingRequestService-SmartCharging.test.ts index c69dd972..51236ab2 100644 --- a/tests/charging-station/ocpp/1.6/OCPP16IncomingRequestService-SmartCharging.test.ts +++ b/tests/charging-station/ocpp/1.6/OCPP16IncomingRequestService-SmartCharging.test.ts @@ -213,7 +213,7 @@ await describe('OCPP16IncomingRequestService — SmartCharging', async () => { 'Core,SmartCharging' ) // Ensure no profiles on any connector - for (const [connectorId] of station.connectors.entries()) { + for (const { connectorId } of station.iterateConnectors()) { const connectorStatus = station.getConnectorStatus(connectorId) if (connectorStatus != null) { connectorStatus.chargingProfiles = [] diff --git a/tests/charging-station/ocpp/1.6/OCPP16Integration-Transactions.test.ts b/tests/charging-station/ocpp/1.6/OCPP16Integration-Transactions.test.ts index d100bdd9..0c6fe70b 100644 --- a/tests/charging-station/ocpp/1.6/OCPP16Integration-Transactions.test.ts +++ b/tests/charging-station/ocpp/1.6/OCPP16Integration-Transactions.test.ts @@ -91,12 +91,10 @@ function createIntegrationContext (): { ) // Add MeterValues template required by buildTransactionBeginMeterValue - for (const [connectorId] of station.connectors) { - if (connectorId > 0) { - const connectorStatus = station.getConnectorStatus(connectorId) - if (connectorStatus != null) { - connectorStatus.MeterValues = [{ unit: OCPP16MeterValueUnit.WATT_HOUR, value: '0' }] - } + for (const { connectorId } of station.iterateConnectors(true)) { + const connectorStatus = station.getConnectorStatus(connectorId) + if (connectorStatus != null) { + connectorStatus.MeterValues = [{ unit: OCPP16MeterValueUnit.WATT_HOUR, value: '0' }] } } diff --git a/tests/charging-station/ocpp/1.6/OCPP16ResponseService-Transactions.test.ts b/tests/charging-station/ocpp/1.6/OCPP16ResponseService-Transactions.test.ts index a59a7f8b..d389eaa4 100644 --- a/tests/charging-station/ocpp/1.6/OCPP16ResponseService-Transactions.test.ts +++ b/tests/charging-station/ocpp/1.6/OCPP16ResponseService-Transactions.test.ts @@ -50,12 +50,10 @@ await describe('OCPP16ResponseService — StartTransaction and StopTransaction', }) // Add MeterValues template required by buildTransactionBeginMeterValue - for (const [connectorId] of station.connectors) { - if (connectorId > 0) { - const connectorStatus = station.getConnectorStatus(connectorId) - if (connectorStatus != null) { - connectorStatus.MeterValues = [{ unit: OCPP16MeterValueUnit.WATT_HOUR, value: '0' }] - } + for (const { connectorId } of station.iterateConnectors(true)) { + const connectorStatus = station.getConnectorStatus(connectorId) + if (connectorStatus != null) { + connectorStatus.MeterValues = [{ unit: OCPP16MeterValueUnit.WATT_HOUR, value: '0' }] } } }) diff --git a/tests/charging-station/ocpp/1.6/OCPP16TestUtils.ts b/tests/charging-station/ocpp/1.6/OCPP16TestUtils.ts index 4bee624a..f435e3de 100644 --- a/tests/charging-station/ocpp/1.6/OCPP16TestUtils.ts +++ b/tests/charging-station/ocpp/1.6/OCPP16TestUtils.ts @@ -298,8 +298,7 @@ export async function dispatchResponse ( * @param chargingStation - Charging station instance whose connector state should be reset */ export function resetConnectorTransactionState (chargingStation: ChargingStation): void { - for (const [connectorId, connectorStatus] of chargingStation.connectors.entries()) { - if (connectorId === 0) continue + for (const { connectorStatus } of chargingStation.iterateConnectors(true)) { connectorStatus.transactionStarted = false connectorStatus.transactionId = undefined connectorStatus.transactionIdTag = undefined diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ChangeAvailability.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ChangeAvailability.test.ts index b64ceabb..190ee6f0 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ChangeAvailability.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ChangeAvailability.test.ts @@ -71,14 +71,12 @@ await describe('G03 - ChangeAvailability', async () => { }) assert.strictEqual(response.status, ChangeAvailabilityStatusEnumType.Accepted) - for (const [evseId, evseStatus] of station.evses) { - if (evseId > 0) { - assert.strictEqual( - evseStatus.availability, - OCPP20OperationalStatusEnumType.Inoperative, - `EVSE ${String(evseId)} should be Inoperative` - ) - } + for (const { evseId, evseStatus } of station.iterateEvses(true)) { + assert.strictEqual( + evseStatus.availability, + OCPP20OperationalStatusEnumType.Inoperative, + `EVSE ${String(evseId)} should be Inoperative` + ) } }) @@ -157,10 +155,8 @@ await describe('G03 - ChangeAvailability', async () => { }) assert.strictEqual(response.status, ChangeAvailabilityStatusEnumType.Accepted) - for (const [evseId, evseStatus] of station.evses) { - if (evseId > 0) { - assert.strictEqual(evseStatus.availability, OCPP20OperationalStatusEnumType.Inoperative) - } + for (const { evseStatus } of station.iterateEvses(true)) { + assert.strictEqual(evseStatus.availability, OCPP20OperationalStatusEnumType.Inoperative) } }) }) diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RemoteStartAuth.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RemoteStartAuth.test.ts index 8df12ffa..6c46f17b 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RemoteStartAuth.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RemoteStartAuth.test.ts @@ -22,43 +22,33 @@ import { RequestStartStopStatusEnumType, } from '../../../../src/types/index.js' import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js' +import { + cleanupChargingStation, + createMockChargingStation, +} from '../../ChargingStationTestUtils.js' await describe('G03 - Remote Start Pre-Authorization', async () => { let service: OCPP20IncomingRequestService | undefined let mockStation: ChargingStation | undefined beforeEach(() => { - // Mock charging station with EVSE configuration - mockStation = { - evses: new Map([ - [ - 1, - { - connectors: new Map([[1, { status: ConnectorStatusEnum.Available }]]), - }, - ], - ]), - getConnectorStatus: (_connectorId: number): ConnectorStatus => ({ - availability: OCPP20OperationalStatusEnumType.Operative, - MeterValues: [], - status: ConnectorStatusEnum.Available, - transactionId: undefined, - transactionIdTag: undefined, - transactionStart: undefined, - transactionStarted: false, - }), - inAcceptedState: () => true, - logPrefix: () => '[TEST-STATION-REMOTE-START]', + const { station } = createMockChargingStation({ + connectorsCount: 1, + evseConfiguration: { evsesCount: 1 }, + ocppVersion: OCPPVersion.VERSION_201, stationInfo: { chargingStationId: 'TEST-REMOTE-START', - ocppVersion: OCPPVersion.VERSION_201, }, - } as unknown as ChargingStation + }) + mockStation = station service = new OCPP20IncomingRequestService() }) afterEach(() => { + if (mockStation != null) { + cleanupChargingStation(mockStation) + } standardCleanup() mockStation = undefined service = undefined @@ -431,8 +421,8 @@ await describe('G03 - Remote Start Pre-Authorization', async () => { assert(mockStation != null) // Then: Charging station should have required configuration assert.notStrictEqual(mockStation, undefined) - assert.notStrictEqual(mockStation.evses, undefined) - assert.ok(mockStation.evses.size > 0) + assert.notStrictEqual(mockStation.getNumberOfEvses(), 0) + assert.ok(mockStation.getNumberOfEvses() > 0) assert.strictEqual(mockStation.stationInfo?.ocppVersion, OCPPVersion.VERSION_201) }) }) diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStartTransaction.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStartTransaction.test.ts index 51f02222..058ecff9 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStartTransaction.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStartTransaction.test.ts @@ -13,6 +13,7 @@ import type { OCPP20ChargingRateUnitEnumType, OCPP20RequestStartTransactionRequest, OCPP20RequestStartTransactionResponse, + OCPP20TransactionEventOptions, OCPP20TransactionEventRequest, } from '../../../../src/types/index.js' @@ -521,6 +522,11 @@ await describe('F01 & F02 - Remote Start Transaction', async () => { ] const transactionEvent = args[2] assert.strictEqual(transactionEvent.triggerReason, OCPP20TriggerReasonEnumType.RemoteStart) + // F01.FR.25: remoteStartId SHALL be included in TransactionEventRequest + assert.strictEqual( + (transactionEvent as unknown as OCPP20TransactionEventOptions).remoteStartId, + 3 + ) }) await it('should handle TransactionEvent failure gracefully', async () => { diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-Reset.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-Reset.test.ts index 6ce2be69..0bfd679c 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-Reset.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-Reset.test.ts @@ -436,7 +436,7 @@ await describe('B11 & B12 - Reset', async () => { } // Assign reservation to first connector - const evse: EvseStatus | undefined = station.evses.get(1) + const evse: EvseStatus | undefined = station.getEvseStatus(1) if (evse) { const connectorId = [...evse.connectors.keys()][0] const connectorStatus = evse.connectors.get(connectorId) @@ -469,7 +469,7 @@ await describe('B11 & B12 - Reset', async () => { } // Assign expired reservation to first connector - const evse: EvseStatus | undefined = station.evses.get(1) + const evse: EvseStatus | undefined = station.getEvseStatus(1) if (evse) { const connectorId = [...evse.connectors.keys()][0] const connectorStatus = evse.connectors.get(connectorId) @@ -550,7 +550,7 @@ await describe('B11 & B12 - Reset', async () => { id: 1, idTag: 'test-tag', } - const evse: EvseStatus | undefined = station.evses.get(1) + const evse: EvseStatus | undefined = station.getEvseStatus(1) if (evse) { const connectorId = [...evse.connectors.keys()][0] const connectorStatus = evse.connectors.get(connectorId) diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-UnlockConnector.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-UnlockConnector.test.ts index 936f3cba..8a5f851e 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-UnlockConnector.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-UnlockConnector.test.ts @@ -137,7 +137,7 @@ await describe('F05 - UnlockConnector', async () => { await it('should return OngoingAuthorizedTransaction when specified connector has active transaction', async () => { const { mockStation } = createUnlockConnectorStation() - const evseStatus = mockStation.evses.get(1) + const evseStatus = mockStation.getEvseStatus(1) const connectorStatus = evseStatus?.connectors.get(1) if (connectorStatus != null) { connectorStatus.transactionId = 'tx-001' @@ -175,7 +175,7 @@ await describe('F05 - UnlockConnector', async () => { }) const multiConnectorStation = station as MockChargingStation - const evseStatus = multiConnectorStation.evses.get(1) + const evseStatus = multiConnectorStation.getEvseStatus(1) const connector2 = evseStatus?.connectors.get(2) if (connector2 != null) { connector2.transactionId = 'tx-other' diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-UpdateFirmware.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-UpdateFirmware.test.ts index 121a0f69..f3f83a5b 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-UpdateFirmware.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-UpdateFirmware.test.ts @@ -174,7 +174,7 @@ await describe('L01/L02 - UpdateFirmware', async () => { }) // Set an active transaction on EVSE 1's connector - const evse1 = evseStation.evses.get(1) + const evse1 = evseStation.getEvseStatus(1) if (evse1 != null) { const firstConnector = evse1.connectors.values().next().value if (firstConnector != null) { diff --git a/tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts b/tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts index a346c07d..3ceed9d9 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts @@ -225,7 +225,7 @@ export function createTestableOCPP20RequestService ( */ export function resetConnectorTransactionState (chargingStation: ChargingStation): void { if (chargingStation.hasEvses) { - for (const evseStatus of chargingStation.evses.values()) { + for (const { evseStatus } of chargingStation.iterateEvses()) { for (const connectorStatus of evseStatus.connectors.values()) { connectorStatus.transactionStarted = false connectorStatus.transactionId = undefined @@ -239,8 +239,7 @@ export function resetConnectorTransactionState (chargingStation: ChargingStation } } } else { - for (const [connectorId, connectorStatus] of chargingStation.connectors.entries()) { - if (connectorId === 0) continue // Skip connector 0 (charging station itself) + for (const { connectorStatus } of chargingStation.iterateConnectors(true)) { connectorStatus.transactionStarted = false connectorStatus.transactionId = undefined connectorStatus.transactionIdTag = undefined diff --git a/tests/utils/ChargingStationConfigurationUtils.test.ts b/tests/utils/ChargingStationConfigurationUtils.test.ts index a7625eca..6e2a7ac9 100644 --- a/tests/utils/ChargingStationConfigurationUtils.test.ts +++ b/tests/utils/ChargingStationConfigurationUtils.test.ts @@ -9,7 +9,7 @@ import assert from 'node:assert/strict' import { afterEach, describe, it } from 'node:test' import type { ChargingStation } from '../../src/charging-station/index.js' -import type { ConnectorStatus, EvseStatus } from '../../src/types/index.js' +import type { ConnectorEntry, ConnectorStatus, EvseStatus } from '../../src/types/index.js' import { AvailabilityType } from '../../src/types/index.js' import { @@ -20,34 +20,29 @@ import { buildEvseEntries, buildEvsesStatus, } from '../../src/utils/ChargingStationConfigurationUtils.js' +import { + cleanupChargingStation, + createMockChargingStation, +} from '../charging-station/ChargingStationTestUtils.js' import { standardCleanup } from '../helpers/TestLifecycleHelpers.js' /** - * Creates a minimal mock ChargingStation for configuration utility tests. - * @param options - Mock station properties. - * @param options.automaticTransactionGenerator - ATG instance stub. - * @param options.connectors - Connectors map. - * @param options.evses - EVSEs map. - * @param options.getAutomaticTransactionGeneratorConfiguration - ATG config getter. - * @returns Partial ChargingStation cast for test use. + * Typed access to mock station internals that are private on ChargingStation. + * The mock factory creates a plain object where these are regular properties. */ -function createMockStationForConfigUtils (options: { - automaticTransactionGenerator?: undefined | { connectorsStatus?: Map } - connectors?: Map - evses?: Map - getAutomaticTransactionGeneratorConfiguration?: () => unknown -}): ChargingStation { - return { - automaticTransactionGenerator: options.automaticTransactionGenerator ?? undefined, - connectors: options.connectors ?? new Map(), - evses: options.evses ?? new Map(), - getAutomaticTransactionGeneratorConfiguration: - options.getAutomaticTransactionGeneratorConfiguration ?? (() => undefined), - } as unknown as ChargingStation +interface MockStationInternals { + connectors: Map + evses: Map } await describe('ChargingStationConfigurationUtils', async () => { + let testStation: ChargingStation | undefined + afterEach(() => { + if (testStation != null) { + cleanupChargingStation(testStation) + testStation = undefined + } standardCleanup() }) @@ -61,12 +56,15 @@ await describe('ChargingStationConfigurationUtils', async () => { const interval1 = setInterval(noop, 1000) const interval2 = setInterval(noop, 1000) try { - const connectors = new Map() - connectors.set(0, { + const { station } = createMockChargingStation({ connectorsCount: 0 }) + testStation = station + const internals = station as unknown as MockStationInternals + internals.connectors.clear() + internals.connectors.set(0, { availability: AvailabilityType.Operative, MeterValues: [], } as ConnectorStatus) - connectors.set(1, { + internals.connectors.set(1, { availability: AvailabilityType.Operative, bootStatus: 'Available', MeterValues: [], @@ -76,7 +74,6 @@ await describe('ChargingStationConfigurationUtils', async () => { transactionUpdatedMeterValuesSetInterval: interval1 as unknown as NodeJS.Timeout, } as unknown as ConnectorStatus) - const station = createMockStationForConfigUtils({ connectors }) const result = buildConnectorsStatus(station) assert.strictEqual(result.length, 2) @@ -96,14 +93,19 @@ await describe('ChargingStationConfigurationUtils', async () => { }) await it('should handle empty connectors map', () => { - const station = createMockStationForConfigUtils({ connectors: new Map() }) + const { station } = createMockChargingStation({ connectorsCount: 0 }) + testStation = station + ;(station as unknown as MockStationInternals).connectors.clear() const result = buildConnectorsStatus(station) assert.strictEqual(result.length, 0) }) await it('should preserve non-internal fields', () => { - const connectors = new Map() - connectors.set(1, { + const { station } = createMockChargingStation({ connectorsCount: 0 }) + testStation = station + const internals = station as unknown as MockStationInternals + internals.connectors.clear() + internals.connectors.set(1, { availability: AvailabilityType.Operative, bootStatus: 'Available', MeterValues: [], @@ -113,7 +115,6 @@ await describe('ChargingStationConfigurationUtils', async () => { transactionUpdatedMeterValuesSetInterval: undefined, } as unknown as ConnectorStatus) - const station = createMockStationForConfigUtils({ connectors }) const result = buildConnectorsStatus(station) assert.strictEqual(result.length, 1) @@ -124,17 +125,19 @@ await describe('ChargingStationConfigurationUtils', async () => { }) await it('should preserve non-sequential connector IDs', () => { - const connectors = new Map() - connectors.set(0, { + const { station } = createMockChargingStation({ connectorsCount: 0 }) + testStation = station + const internals = station as unknown as MockStationInternals + internals.connectors.clear() + internals.connectors.set(0, { availability: AvailabilityType.Operative, MeterValues: [], } as ConnectorStatus) - connectors.set(3, { + internals.connectors.set(3, { availability: AvailabilityType.Inoperative, MeterValues: [], } as ConnectorStatus) - const station = createMockStationForConfigUtils({ connectors }) const result = buildConnectorsStatus(station) assert.strictEqual(result.length, 2) @@ -146,6 +149,14 @@ await describe('ChargingStationConfigurationUtils', async () => { await describe('buildEvsesStatus', async () => { await it('should return configuration format with connectorsStatus and without connectors', () => { + const { station } = createMockChargingStation({ + connectorsCount: 1, + evseConfiguration: { evsesCount: 1 }, + }) + testStation = station + const internals = station as unknown as MockStationInternals + internals.evses.clear() + const evseConnectors = new Map() evseConnectors.set(1, { availability: AvailabilityType.Operative, @@ -154,17 +165,15 @@ await describe('ChargingStationConfigurationUtils', async () => { transactionUpdatedMeterValuesSetInterval: undefined, } as unknown as ConnectorStatus) - const evses = new Map() - evses.set(0, { + internals.evses.set(0, { availability: AvailabilityType.Operative, connectors: new Map(), }) - evses.set(1, { + internals.evses.set(1, { availability: AvailabilityType.Operative, connectors: evseConnectors, }) - const station = createMockStationForConfigUtils({ evses }) const result = buildEvsesStatus(station) assert.strictEqual(result.length, 2) @@ -178,6 +187,14 @@ await describe('ChargingStationConfigurationUtils', async () => { }) await it('should strip internal fields from evse connectors', () => { + const { station } = createMockChargingStation({ + connectorsCount: 1, + evseConfiguration: { evsesCount: 1 }, + }) + testStation = station + const internals = station as unknown as MockStationInternals + internals.evses.clear() + const evseConnectors = new Map() evseConnectors.set(1, { availability: AvailabilityType.Operative, @@ -188,17 +205,15 @@ await describe('ChargingStationConfigurationUtils', async () => { transactionUpdatedMeterValuesSetInterval: undefined, } as unknown as ConnectorStatus) - const evses = new Map() - evses.set(0, { + internals.evses.set(0, { availability: AvailabilityType.Operative, connectors: new Map(), }) - evses.set(1, { + internals.evses.set(1, { availability: AvailabilityType.Operative, connectors: evseConnectors, }) - const station = createMockStationForConfigUtils({ evses }) const result = buildEvsesStatus(station) const evse1 = result.find(([id]) => id === 1)?.[1] assert.ok(evse1 != null) @@ -214,6 +229,14 @@ await describe('ChargingStationConfigurationUtils', async () => { }) await it('should preserve connector IDs across serialization', () => { + const { station } = createMockChargingStation({ + connectorsCount: 1, + evseConfiguration: { evsesCount: 1 }, + }) + testStation = station + const internals = station as unknown as MockStationInternals + internals.evses.clear() + const evseConnectors = new Map() evseConnectors.set(1, { availability: AvailabilityType.Operative, @@ -230,17 +253,15 @@ await describe('ChargingStationConfigurationUtils', async () => { MeterValues: [], } as ConnectorStatus) - const evses = new Map() - evses.set(0, { + internals.evses.set(0, { availability: AvailabilityType.Operative, connectors: evse0Connectors, }) - evses.set(1, { + internals.evses.set(1, { availability: AvailabilityType.Operative, connectors: evseConnectors, }) - const station = createMockStationForConfigUtils({ evses }) const result = buildEvsesStatus(station) const evse0Status = result[0][1].connectorsStatus as [number, ConnectorStatus][] @@ -255,23 +276,33 @@ await describe('ChargingStationConfigurationUtils', async () => { }) await it('should handle empty evses map', () => { - const station = createMockStationForConfigUtils({ evses: new Map() }) + const { station } = createMockChargingStation({ + connectorsCount: 1, + evseConfiguration: { evsesCount: 1 }, + }) + testStation = station + ;(station as unknown as MockStationInternals).evses.clear() const result = buildEvsesStatus(station) assert.strictEqual(result.length, 0) }) await it('should preserve non-sequential evse IDs', () => { - const evses = new Map() - evses.set(0, { + const { station } = createMockChargingStation({ + connectorsCount: 1, + evseConfiguration: { evsesCount: 1 }, + }) + testStation = station + const internals = station as unknown as MockStationInternals + internals.evses.clear() + internals.evses.set(0, { availability: AvailabilityType.Operative, connectors: new Map(), }) - evses.set(3, { + internals.evses.set(3, { availability: AvailabilityType.Inoperative, connectors: new Map(), }) - const station = createMockStationForConfigUtils({ evses }) const result = buildEvsesStatus(station) assert.strictEqual(result.length, 2) @@ -284,11 +315,15 @@ await describe('ChargingStationConfigurationUtils', async () => { await describe('buildChargingStationAutomaticTransactionGeneratorConfiguration', async () => { await it('should return ATG configuration when present', () => { const atgConfiguration = { enable: true, maxDuration: 120, minDuration: 60 } - const station = createMockStationForConfigUtils({ - automaticTransactionGenerator: { - connectorsStatus: new Map([[1, { start: false }]]), - }, - getAutomaticTransactionGeneratorConfiguration: () => atgConfiguration, + const { station } = createMockChargingStation({ connectorsCount: 0 }) + testStation = station + station.automaticTransactionGenerator = { + connectorsStatus: new Map([[1, { start: false }]]), + } as unknown as typeof station.automaticTransactionGenerator + Object.defineProperty(station, 'getAutomaticTransactionGeneratorConfiguration', { + configurable: true, + value: () => atgConfiguration, + writable: true, }) const result = buildChargingStationAutomaticTransactionGeneratorConfiguration(station) @@ -300,9 +335,13 @@ await describe('ChargingStationConfigurationUtils', async () => { await it('should return ATG configuration without statuses when no ATG instance', () => { const atgConfiguration = { enable: false } - const station = createMockStationForConfigUtils({ - automaticTransactionGenerator: undefined, - getAutomaticTransactionGeneratorConfiguration: () => atgConfiguration, + const { station } = createMockChargingStation({ connectorsCount: 0 }) + testStation = station + station.automaticTransactionGenerator = undefined + Object.defineProperty(station, 'getAutomaticTransactionGeneratorConfiguration', { + configurable: true, + value: () => atgConfiguration, + writable: true, }) const result = buildChargingStationAutomaticTransactionGeneratorConfiguration(station) @@ -311,9 +350,13 @@ await describe('ChargingStationConfigurationUtils', async () => { }) await it('should return undefined ATG config when not configured', () => { - const station = createMockStationForConfigUtils({ - automaticTransactionGenerator: undefined, - getAutomaticTransactionGeneratorConfiguration: () => undefined, + const { station } = createMockChargingStation({ connectorsCount: 0 }) + testStation = station + station.automaticTransactionGenerator = undefined + Object.defineProperty(station, 'getAutomaticTransactionGeneratorConfiguration', { + configurable: true, + value: () => undefined, + writable: true, }) const result = buildChargingStationAutomaticTransactionGeneratorConfiguration(station) @@ -323,11 +366,15 @@ await describe('ChargingStationConfigurationUtils', async () => { await it('should return ATG configuration without statuses when connectorsStatus is null', () => { const atgConfiguration = { enable: true } - const station = createMockStationForConfigUtils({ - automaticTransactionGenerator: { - connectorsStatus: undefined, - }, - getAutomaticTransactionGeneratorConfiguration: () => atgConfiguration, + const { station } = createMockChargingStation({ connectorsCount: 0 }) + testStation = station + station.automaticTransactionGenerator = { + connectorsStatus: undefined, + } as unknown as typeof station.automaticTransactionGenerator + Object.defineProperty(station, 'getAutomaticTransactionGeneratorConfiguration', { + configurable: true, + value: () => atgConfiguration, + writable: true, }) const result = buildChargingStationAutomaticTransactionGeneratorConfiguration(station) @@ -344,9 +391,11 @@ await describe('ChargingStationConfigurationUtils', async () => { connectorsStatus.set(1, { start: true }) connectorsStatus.set(3, { start: false }) - const station = createMockStationForConfigUtils({ - automaticTransactionGenerator: { connectorsStatus }, - }) + const { station } = createMockChargingStation({ connectorsCount: 0 }) + testStation = station + station.automaticTransactionGenerator = { + connectorsStatus, + } as unknown as typeof station.automaticTransactionGenerator const result = buildATGEntries(station) assert.strictEqual(result.length, 2) @@ -361,9 +410,11 @@ await describe('ChargingStationConfigurationUtils', async () => { connectorsStatus.set(2, { start: true }) connectorsStatus.set(7, { start: false }) - const station = createMockStationForConfigUtils({ - automaticTransactionGenerator: { connectorsStatus }, - }) + const { station } = createMockChargingStation({ connectorsCount: 0 }) + testStation = station + station.automaticTransactionGenerator = { + connectorsStatus, + } as unknown as typeof station.automaticTransactionGenerator const result = buildATGEntries(station) assert.strictEqual(result.length, 2) @@ -372,30 +423,35 @@ await describe('ChargingStationConfigurationUtils', async () => { }) await it('should return empty array when no ATG instance', () => { - const station = createMockStationForConfigUtils({ - automaticTransactionGenerator: undefined, - }) + const { station } = createMockChargingStation({ connectorsCount: 0 }) + testStation = station + station.automaticTransactionGenerator = undefined const result = buildATGEntries(station) assert.strictEqual(result.length, 0) }) await it('should return empty array when connectorsStatus is undefined', () => { - const station = createMockStationForConfigUtils({ - automaticTransactionGenerator: { connectorsStatus: undefined }, - }) + const { station } = createMockChargingStation({ connectorsCount: 0 }) + testStation = station + station.automaticTransactionGenerator = { + connectorsStatus: undefined, + } as unknown as typeof station.automaticTransactionGenerator const result = buildATGEntries(station) assert.strictEqual(result.length, 0) }) }) await describe('buildConnectorEntries', async () => { - await it('should return entries with connectorId and stripped connector', () => { - const connectors = new Map() - connectors.set(0, { + await it('should return entries with connectorId, connectorStatus, and evseId', () => { + const { station } = createMockChargingStation({ connectorsCount: 0 }) + testStation = station + const internals = station as unknown as MockStationInternals + internals.connectors.clear() + internals.connectors.set(0, { availability: AvailabilityType.Operative, MeterValues: [], } as ConnectorStatus) - connectors.set(1, { + internals.connectors.set(1, { availability: AvailabilityType.Operative, MeterValues: [], transactionEndedMeterValues: [{ sampledValue: [], timestamp: new Date() }], @@ -404,53 +460,66 @@ await describe('ChargingStationConfigurationUtils', async () => { transactionUpdatedMeterValuesSetInterval: undefined, } as unknown as ConnectorStatus) - const station = createMockStationForConfigUtils({ connectors }) const result = buildConnectorEntries(station) assert.strictEqual(result.length, 2) assert.strictEqual(result[0].connectorId, 0) + assert.strictEqual(result[0].evseId, undefined) assert.strictEqual(result[1].connectorId, 1) - assert.strictEqual(result[1].connector.availability, AvailabilityType.Operative) - assert.ok(!('transactionEndedMeterValues' in result[1].connector)) - assert.ok(!('transactionEndedMeterValuesSetInterval' in result[1].connector)) - assert.ok(!('transactionUpdatedMeterValuesSetInterval' in result[1].connector)) - assert.ok(!('transactionEventQueue' in result[1].connector)) + assert.strictEqual(result[1].evseId, undefined) + assert.strictEqual(result[1].connectorStatus.availability, AvailabilityType.Operative) + assert.ok(!('transactionEndedMeterValues' in result[1].connectorStatus)) + assert.ok(!('transactionEndedMeterValuesSetInterval' in result[1].connectorStatus)) + assert.ok(!('transactionUpdatedMeterValuesSetInterval' in result[1].connectorStatus)) + assert.ok(!('transactionEventQueue' in result[1].connectorStatus)) }) await it('should handle empty connectors map', () => { - const station = createMockStationForConfigUtils({ connectors: new Map() }) + const { station } = createMockChargingStation({ connectorsCount: 0 }) + testStation = station + ;(station as unknown as MockStationInternals).connectors.clear() const result = buildConnectorEntries(station) assert.strictEqual(result.length, 0) }) await it('should preserve non-sequential connector IDs', () => { - const connectors = new Map() - connectors.set(0, { + const { station } = createMockChargingStation({ connectorsCount: 0 }) + testStation = station + const internals = station as unknown as MockStationInternals + internals.connectors.clear() + internals.connectors.set(0, { availability: AvailabilityType.Operative, MeterValues: [], } as ConnectorStatus) - connectors.set(3, { + internals.connectors.set(3, { availability: AvailabilityType.Operative, MeterValues: [], } as ConnectorStatus) - connectors.set(7, { + internals.connectors.set(7, { availability: AvailabilityType.Inoperative, MeterValues: [], } as ConnectorStatus) - const station = createMockStationForConfigUtils({ connectors }) const result = buildConnectorEntries(station) assert.strictEqual(result.length, 3) assert.strictEqual(result[0].connectorId, 0) assert.strictEqual(result[1].connectorId, 3) assert.strictEqual(result[2].connectorId, 7) - assert.strictEqual(result[2].connector.availability, AvailabilityType.Inoperative) + assert.strictEqual(result[2].connectorStatus.availability, AvailabilityType.Inoperative) }) }) await describe('buildEvseEntries', async () => { - await it('should return entries with evseId, availability, and connector entries', () => { + await it('should return entries with evseId, evseStatus containing availability and connectors Map', () => { + const { station } = createMockChargingStation({ + connectorsCount: 1, + evseConfiguration: { evsesCount: 1 }, + }) + testStation = station + const internals = station as unknown as MockStationInternals + internals.evses.clear() + const evseConnectors = new Map() evseConnectors.set(1, { availability: AvailabilityType.Operative, @@ -461,39 +530,51 @@ await describe('ChargingStationConfigurationUtils', async () => { transactionUpdatedMeterValuesSetInterval: undefined, } as unknown as ConnectorStatus) - const evses = new Map() - evses.set(0, { + internals.evses.set(0, { availability: AvailabilityType.Operative, connectors: new Map(), }) - evses.set(1, { + internals.evses.set(1, { availability: AvailabilityType.Operative, connectors: evseConnectors, }) - const station = createMockStationForConfigUtils({ evses }) const result = buildEvseEntries(station) assert.strictEqual(result.length, 2) assert.strictEqual(result[0].evseId, 0) - assert.strictEqual(result[0].availability, AvailabilityType.Operative) - assert.strictEqual(result[0].connectors.length, 0) + assert.strictEqual(result[0].evseStatus.availability, AvailabilityType.Operative) + assert.strictEqual((result[0].evseStatus.connectors as unknown as ConnectorEntry[]).length, 0) assert.strictEqual(result[1].evseId, 1) - assert.strictEqual(result[1].connectors.length, 1) - assert.strictEqual(result[1].connectors[0].connectorId, 1) - assert.ok(!('transactionEndedMeterValues' in result[1].connectors[0].connector)) - assert.ok(!('transactionEndedMeterValuesSetInterval' in result[1].connectors[0].connector)) - assert.ok(!('transactionUpdatedMeterValuesSetInterval' in result[1].connectors[0].connector)) - assert.ok(!('transactionEventQueue' in result[1].connectors[0].connector)) + const connectors1 = result[1].evseStatus.connectors as unknown as ConnectorEntry[] + assert.strictEqual(connectors1.length, 1) + assert.strictEqual(connectors1[0].connectorId, 1) + assert.ok(!('transactionEndedMeterValues' in connectors1[0].connectorStatus)) + assert.ok(!('transactionEndedMeterValuesSetInterval' in connectors1[0].connectorStatus)) + assert.ok(!('transactionUpdatedMeterValuesSetInterval' in connectors1[0].connectorStatus)) + assert.ok(!('transactionEventQueue' in connectors1[0].connectorStatus)) }) await it('should handle empty evses map', () => { - const station = createMockStationForConfigUtils({ evses: new Map() }) + const { station } = createMockChargingStation({ + connectorsCount: 1, + evseConfiguration: { evsesCount: 1 }, + }) + testStation = station + ;(station as unknown as MockStationInternals).evses.clear() const result = buildEvseEntries(station) assert.strictEqual(result.length, 0) }) await it('should preserve non-sequential evseId and connectorId', () => { + const { station } = createMockChargingStation({ + connectorsCount: 1, + evseConfiguration: { evsesCount: 1 }, + }) + testStation = station + const internals = station as unknown as MockStationInternals + internals.evses.clear() + const evse2Connectors = new Map() evse2Connectors.set(2, { availability: AvailabilityType.Operative, @@ -504,29 +585,26 @@ await describe('ChargingStationConfigurationUtils', async () => { MeterValues: [], } as ConnectorStatus) - const evses = new Map() - evses.set(0, { + internals.evses.set(0, { availability: AvailabilityType.Operative, connectors: new Map(), }) - evses.set(3, { + internals.evses.set(3, { availability: AvailabilityType.Operative, connectors: evse2Connectors, }) - const station = createMockStationForConfigUtils({ evses }) const result = buildEvseEntries(station) assert.strictEqual(result.length, 2) assert.strictEqual(result[0].evseId, 0) assert.strictEqual(result[1].evseId, 3) - assert.strictEqual(result[1].connectors.length, 2) - assert.strictEqual(result[1].connectors[0].connectorId, 2) - assert.strictEqual(result[1].connectors[1].connectorId, 5) - assert.strictEqual( - result[1].connectors[1].connector.availability, - AvailabilityType.Inoperative - ) + const connectors3 = result[1].evseStatus.connectors as unknown as ConnectorEntry[] + assert.strictEqual(connectors3.length, 2) + assert.ok(connectors3.some(c => c.connectorId === 2)) + assert.ok(connectors3.some(c => c.connectorId === 5)) + const conn5 = connectors3.find(c => c.connectorId === 5) + assert.strictEqual(conn5?.connectorStatus.availability, AvailabilityType.Inoperative) }) }) }) diff --git a/tests/utils/MessageChannelUtils.test.ts b/tests/utils/MessageChannelUtils.test.ts index 7e2be5ae..d4c988ef 100644 --- a/tests/utils/MessageChannelUtils.test.ts +++ b/tests/utils/MessageChannelUtils.test.ts @@ -5,12 +5,12 @@ import { CircularBuffer } from 'mnemonist' import assert from 'node:assert/strict' -import { afterEach, describe, it } from 'node:test' +import { afterEach, beforeEach, describe, it } from 'node:test' import type { ChargingStation } from '../../src/charging-station/index.js' import type { Statistics, TimestampedData } from '../../src/types/index.js' -import { AvailabilityType, ChargingStationWorkerMessageEvents } from '../../src/types/index.js' +import { ChargingStationWorkerMessageEvents } from '../../src/types/index.js' import { buildAddedMessage, buildDeletedMessage, @@ -19,59 +19,36 @@ import { buildStoppedMessage, buildUpdatedMessage, } from '../../src/utils/MessageChannelUtils.js' +import { + cleanupChargingStation, + createMockChargingStation, +} from '../charging-station/ChargingStationTestUtils.js' import { standardCleanup } from '../helpers/TestLifecycleHelpers.js' -/** - * Creates a minimal mock station with properties needed by MessageChannelUtils builders. - * @returns Mock charging station instance - */ -function createMockStationForMessages (): ChargingStation { - return { - automaticTransactionGenerator: undefined, - bootNotificationResponse: { - currentTime: new Date('2024-01-01T00:00:00Z'), - interval: 300, - status: 'Accepted', - }, - connectors: new Map([ - [ - 0, - { - availability: AvailabilityType.Operative, - MeterValues: [], - }, - ], - [ - 1, - { - availability: AvailabilityType.Operative, - MeterValues: [], - }, - ], - ]), - evses: new Map(), - getAutomaticTransactionGeneratorConfiguration: () => undefined, - ocppConfiguration: { configurationKey: [] }, - started: true, - stationInfo: { +await describe('MessageChannelUtils', async () => { + let station: ChargingStation + + beforeEach(() => { + const result = createMockChargingStation({ baseName: 'CS-TEST', - chargingStationId: 'CS-TEST-00001', - hashId: 'test-hash', - templateIndex: 1, - templateName: 'test-template.json', - }, - wsConnection: { readyState: 1 }, - wsConnectionUrl: new URL('ws://localhost:8080/CS-TEST-00001'), - } as unknown as ChargingStation -} + connectorsCount: 1, + started: true, + stationInfo: { hashId: 'test-hash' }, + }) + station = result.station + // Add wsConnectionUrl getter needed by MessageChannelUtils builders + Object.defineProperty(station, 'wsConnectionUrl', { + configurable: true, + get: () => new URL('ws://localhost:8080/CS-TEST-00001'), + }) + }) -await describe('MessageChannelUtils', async () => { afterEach(() => { + cleanupChargingStation(station) standardCleanup() }) await it('should build added message with correct event and data', () => { - const station = createMockStationForMessages() const message = buildAddedMessage(station) assert.strictEqual(message.event, ChargingStationWorkerMessageEvents.added) @@ -83,7 +60,6 @@ await describe('MessageChannelUtils', async () => { }) await it('should build deleted message with correct event', () => { - const station = createMockStationForMessages() const message = buildDeletedMessage(station) assert.strictEqual(message.event, ChargingStationWorkerMessageEvents.deleted) @@ -92,7 +68,6 @@ await describe('MessageChannelUtils', async () => { }) await it('should build started message with correct event', () => { - const station = createMockStationForMessages() const message = buildStartedMessage(station) assert.strictEqual(message.event, ChargingStationWorkerMessageEvents.started) @@ -100,7 +75,6 @@ await describe('MessageChannelUtils', async () => { }) await it('should build stopped message with correct event', () => { - const station = createMockStationForMessages() const message = buildStoppedMessage(station) assert.strictEqual(message.event, ChargingStationWorkerMessageEvents.stopped) @@ -109,7 +83,6 @@ await describe('MessageChannelUtils', async () => { }) await it('should build updated message with correct event', () => { - const station = createMockStationForMessages() const message = buildUpdatedMessage(station) assert.strictEqual(message.event, ChargingStationWorkerMessageEvents.updated) @@ -118,14 +91,12 @@ await describe('MessageChannelUtils', async () => { }) await it('should include ws state in station messages', () => { - const station = createMockStationForMessages() const message = buildAddedMessage(station) assert.strictEqual(message.data.wsState, 1) }) await it('should include connectors status in station messages', () => { - const station = createMockStationForMessages() const message = buildAddedMessage(station) assert.ok(Array.isArray(message.data.connectors)) diff --git a/ui/web/src/components/charging-stations/CSData.vue b/ui/web/src/components/charging-stations/CSData.vue index 181f52ac..4da1d553 100644 --- a/ui/web/src/components/charging-stations/CSData.vue +++ b/ui/web/src/components/charging-stations/CSData.vue @@ -120,7 +120,7 @@ :key="entry.evseId != null ? `${entry.evseId}-${entry.connectorId}` : entry.connectorId" :atg-status="getATGStatus(entry.connectorId)" :charging-station-id="chargingStation.stationInfo.chargingStationId" - :connector="entry.connector" + :connector="entry.connectorStatus" :connector-id="entry.connectorId" :evse-id="entry.evseId" :hash-id="chargingStation.stationInfo.hashId" @@ -137,7 +137,7 @@ import { computed } from 'vue' import { useToast } from 'vue-toast-notification' -import type { ChargingStationData, ConnectorStatus, Status } from '@/types' +import type { ChargingStationData, ConnectorEntry, Status } from '@/types' import Button from '@/components/buttons/Button.vue' import StateButton from '@/components/buttons/StateButton.vue' @@ -150,12 +150,6 @@ import { useUIClient, } from '@/composables' -interface ConnectorTableEntry { - connector: ConnectorStatus - connectorId: number - evseId?: number -} - const props = defineProps<{ chargingStation: ChargingStationData }>() @@ -164,16 +158,16 @@ const $emit = defineEmits(['need-refresh']) const isWebSocketOpen = computed(() => props.chargingStation.wsState === WebSocket.OPEN) -const getConnectorEntries = (): ConnectorTableEntry[] => { +const getConnectorEntries = (): ConnectorEntry[] => { if (Array.isArray(props.chargingStation.evses) && props.chargingStation.evses.length > 0) { - const entries: ConnectorTableEntry[] = [] + const entries: ConnectorEntry[] = [] for (const evse of props.chargingStation.evses) { if (evse.evseId > 0) { - for (const entry of evse.connectors) { + for (const entry of evse.evseStatus.connectors) { if (entry.connectorId > 0) { entries.push({ - connector: entry.connector, connectorId: entry.connectorId, + connectorStatus: entry.connectorStatus, evseId: evse.evseId, }) } @@ -185,8 +179,8 @@ const getConnectorEntries = (): ConnectorTableEntry[] => { return (props.chargingStation.connectors ?? []) .filter(c => c.connectorId > 0) .map(entry => ({ - connector: entry.connector, connectorId: entry.connectorId, + connectorStatus: entry.connectorStatus, })) } const getATGStatus = (connectorId: number): Status | undefined => { diff --git a/ui/web/src/types/ChargingStationType.ts b/ui/web/src/types/ChargingStationType.ts index 71d230b9..5c00b160 100644 --- a/ui/web/src/types/ChargingStationType.ts +++ b/ui/web/src/types/ChargingStationType.ts @@ -249,8 +249,9 @@ export interface ConfigurationKey extends OCPPConfigurationKey { } export interface ConnectorEntry extends JsonObject { - connector: ConnectorStatus connectorId: number + connectorStatus: ConnectorStatus + evseId?: number } export interface ConnectorStatus extends JsonObject { @@ -276,9 +277,11 @@ export interface ConnectorStatus extends JsonObject { } export interface EvseEntry extends JsonObject { - availability: AvailabilityType - connectors: ConnectorEntry[] evseId: number + evseStatus: { + availability: AvailabilityType + connectors: ConnectorEntry[] + } } export interface OCPP20EVSEType extends JsonObject { diff --git a/ui/web/tests/unit/CSData.test.ts b/ui/web/tests/unit/CSData.test.ts index f59974a1..303c1cd1 100644 --- a/ui/web/tests/unit/CSData.test.ts +++ b/ui/web/tests/unit/CSData.test.ts @@ -242,9 +242,9 @@ describe('CSData', () => { it('should generate entries from connectors array for OCPP 1.6', () => { const station = createChargingStationData({ connectors: [ - { connector: createConnectorStatus(), connectorId: 0 }, - { connector: createConnectorStatus(), connectorId: 1 }, - { connector: createConnectorStatus(), connectorId: 2 }, + { connectorId: 0, connectorStatus: createConnectorStatus() }, + { connectorId: 1, connectorStatus: createConnectorStatus() }, + { connectorId: 2, connectorStatus: createConnectorStatus() }, ], }) const wrapper = mountCSData(station) @@ -253,7 +253,7 @@ describe('CSData', () => { it('should filter out connector 0', () => { const station = createChargingStationData({ - connectors: [{ connector: createConnectorStatus(), connectorId: 0 }], + connectors: [{ connectorId: 0, connectorStatus: createConnectorStatus() }], }) const wrapper = mountCSData(station) expect(wrapper.findAllComponents(CSConnector)).toHaveLength(0) @@ -264,16 +264,25 @@ describe('CSData', () => { connectors: [], evses: [ createEvseEntry({ - connectors: [{ connector: createConnectorStatus(), connectorId: 0 }], evseId: 0, + evseStatus: { + availability: 'Operative' as never, + connectors: [{ connectorId: 0, connectorStatus: createConnectorStatus() }], + }, }), createEvseEntry({ - connectors: [{ connector: createConnectorStatus(), connectorId: 1 }], evseId: 1, + evseStatus: { + availability: 'Operative' as never, + connectors: [{ connectorId: 1, connectorStatus: createConnectorStatus() }], + }, }), createEvseEntry({ - connectors: [{ connector: createConnectorStatus(), connectorId: 1 }], evseId: 2, + evseStatus: { + availability: 'Operative' as never, + connectors: [{ connectorId: 1, connectorStatus: createConnectorStatus() }], + }, }), ], stationInfo: createStationInfo({ ocppVersion: OCPPVersion.VERSION_201 }), @@ -287,8 +296,11 @@ describe('CSData', () => { connectors: [], evses: [ createEvseEntry({ - connectors: [{ connector: createConnectorStatus(), connectorId: 0 }], evseId: 0, + evseStatus: { + availability: 'Operative' as never, + connectors: [{ connectorId: 0, connectorStatus: createConnectorStatus() }], + }, }), ], stationInfo: createStationInfo({ ocppVersion: OCPPVersion.VERSION_201 }), diff --git a/ui/web/tests/unit/constants.ts b/ui/web/tests/unit/constants.ts index 3a57a109..bb77111d 100644 --- a/ui/web/tests/unit/constants.ts +++ b/ui/web/tests/unit/constants.ts @@ -40,7 +40,7 @@ export function createChargingStationData ( interval: 60, status: OCPP16RegistrationStatus.ACCEPTED, }, - connectors: [{ connector: createConnectorStatus(), connectorId: 1 }], + connectors: [{ connectorId: 1, connectorStatus: createConnectorStatus() }], ocppConfiguration: { configurationKey: [] }, started: true, stationInfo: createStationInfo(), @@ -70,9 +70,11 @@ export function createConnectorStatus (overrides?: Partial): Co */ export function createEvseEntry (overrides?: Partial): EvseEntry { return { - availability: OCPP16AvailabilityType.OPERATIVE, - connectors: [{ connector: createConnectorStatus(), connectorId: 1 }], evseId: 1, + evseStatus: { + availability: OCPP16AvailabilityType.OPERATIVE, + connectors: [{ connectorId: 1, connectorStatus: createConnectorStatus() }], + }, ...overrides, } } -- 2.43.0