import { BaseError } from '../exception/index.js'
import { PerformanceStatistics } from '../performance/index.js'
import {
- AuthorizationStatus,
ChargingStationEvents,
- RequestCommand,
- type StartTransactionRequest,
- type StartTransactionResponse,
+ type StartTransactionResult,
type Status,
StopTransactionReason,
- type StopTransactionResponse,
+ type StopTransactionResult,
} from '../types/index.js'
import {
clone,
import { checkChargingStationState } from './Helpers.js'
import { IdTagsCache } from './IdTagsCache.js'
import { isIdTagAuthorized } from './ocpp/index.js'
+import { startTransactionOnConnector, stopTransactionOnConnector } from './ocpp/OCPPServiceUtils.js'
export class AutomaticTransactionGenerator {
private static readonly instances: Map<string, AutomaticTransactionGenerator> = new Map<
)
}
- private handleStartTransactionResponse (
- connectorId: number,
- startResponse: StartTransactionResponse
- ): void {
+ private handleStartTransactionResult (connectorId: number, result: StartTransactionResult): void {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
++this.connectorsStatus.get(connectorId)!.startTransactionRequests
- if (startResponse.idTagInfo.status === AuthorizationStatus.ACCEPTED) {
+ if (result.accepted) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
++this.connectorsStatus.get(connectorId)!.acceptedStartTransactionRequests
} else {
this.connectorsStatus.get(connectorId)!.skippedConsecutiveTransactions = 0
// Start transaction
const startResponse = await this.startTransaction(connectorId)
- if (startResponse?.idTagInfo.status === AuthorizationStatus.ACCEPTED) {
+ if (startResponse?.accepted === true) {
// Wait until end of transaction
const waitTrxEnd = secondsToMilliseconds(
randomInt(
}
}
- private async startTransaction (
- connectorId: number
- ): Promise<StartTransactionResponse | undefined> {
+ private async startTransaction (connectorId: number): Promise<StartTransactionResult | undefined> {
const measureId = 'StartTransaction with ATG'
const beginId = PerformanceStatistics.beginMeasure(measureId)
- let startResponse: StartTransactionResponse | undefined
+ let result: StartTransactionResult | undefined
if (this.chargingStation.hasIdTags()) {
const idTag = IdTagsCache.getInstance().getIdTag(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
++this.connectorsStatus.get(connectorId)!.acceptedAuthorizeRequests
logger.info(startTransactionLogMsg)
- // Start transaction
- startResponse = await this.chargingStation.ocppRequestService.requestHandler<
- Partial<StartTransactionRequest>,
- StartTransactionResponse
- >(this.chargingStation, RequestCommand.START_TRANSACTION, {
- connectorId,
- idTag,
- })
- this.handleStartTransactionResponse(connectorId, startResponse)
+ result = await startTransactionOnConnector(this.chargingStation, connectorId, idTag)
+ this.handleStartTransactionResult(connectorId, result)
PerformanceStatistics.endMeasure(measureId, beginId)
- return startResponse
+ return result
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
++this.connectorsStatus.get(connectorId)!.rejectedAuthorizeRequests
PerformanceStatistics.endMeasure(measureId, beginId)
- return startResponse
+ return result
}
logger.info(startTransactionLogMsg)
- // Start transaction
- startResponse = await this.chargingStation.ocppRequestService.requestHandler<
- Partial<StartTransactionRequest>,
- StartTransactionResponse
- >(this.chargingStation, RequestCommand.START_TRANSACTION, {
- connectorId,
- idTag,
- })
- this.handleStartTransactionResponse(connectorId, startResponse)
+ result = await startTransactionOnConnector(this.chargingStation, connectorId, idTag)
+ this.handleStartTransactionResult(connectorId, result)
PerformanceStatistics.endMeasure(measureId, beginId)
- return startResponse
+ return result
}
logger.info(`${this.logPrefix(connectorId)} start transaction without an idTag`)
- startResponse = await this.chargingStation.ocppRequestService.requestHandler<
- Partial<StartTransactionRequest>,
- StartTransactionResponse
- >(this.chargingStation, RequestCommand.START_TRANSACTION, {
- connectorId,
- })
- this.handleStartTransactionResponse(connectorId, startResponse)
+ result = await startTransactionOnConnector(this.chargingStation, connectorId)
+ this.handleStartTransactionResult(connectorId, result)
PerformanceStatistics.endMeasure(measureId, beginId)
- return startResponse
+ return result
}
private stopConnectors (): void {
private async stopTransaction (
connectorId: number,
reason = StopTransactionReason.LOCAL
- ): Promise<StopTransactionResponse | undefined> {
+ ): Promise<StopTransactionResult | undefined> {
const measureId = 'StopTransaction with ATG'
const beginId = PerformanceStatistics.beginMeasure(measureId)
- let stopResponse: StopTransactionResponse | undefined
+ let result: StopTransactionResult | undefined
if (this.chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
logger.info(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
.getConnectorStatus(connectorId)
?.transactionId?.toString()}`
)
- // TODO: OCPP 2.0 stations should use OCPP20ServiceUtils.requestStopTransaction() instead
- // See: src/charging-station/ChargingStation.ts#stopRunningTransactionsOCPP20
- stopResponse = await this.chargingStation.stopTransactionOnConnector(connectorId, reason)
+ result = await stopTransactionOnConnector(this.chargingStation, connectorId, reason)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
++this.connectorsStatus.get(connectorId)!.stopTransactionRequests
- if (stopResponse.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
+ if (result.accepted) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
++this.connectorsStatus.get(connectorId)!.acceptedStopTransactionRequests
} else {
)
}
PerformanceStatistics.endMeasure(measureId, beginId)
- return stopResponse
+ return result
}
private async waitChargingStationAvailable (connectorId: number): Promise<void> {
type IncomingRequestCommand,
MessageType,
MeterValueMeasurand,
- type MeterValuesRequest,
- type MeterValuesResponse,
- type OCPP20MeterValue,
- OCPP20ReasonEnumType,
- OCPP20TransactionEventEnumType,
- OCPP20TriggerReasonEnumType,
OCPPVersion,
type OutgoingRequest,
PowerUnits,
StandardParametersKey,
type Status,
type StopTransactionReason,
- type StopTransactionRequest,
- type StopTransactionResponse,
SupervisionUrlDistribution,
SupportedFeatureProfiles,
type Voltage,
} from './Helpers.js'
import { IdTagsCache } from './IdTagsCache.js'
import {
- buildMeterValue,
- buildTransactionEndMeterValue,
getMessageTypeString,
OCPP16IncomingRequestService,
OCPP16RequestService,
OCPP20IncomingRequestService,
OCPP20RequestService,
OCPP20ResponseService,
- OCPP20ServiceUtils,
OCPPAuthServiceFactory,
type OCPPIncomingRequestService,
type OCPPRequestService,
sendAndSetConnectorStatus,
} from './ocpp/index.js'
+import { flushQueuedTransactionMessages, stopRunningTransactions } from './ocpp/OCPPServiceUtils.js'
import { SharedLRUCache } from './SharedLRUCache.js'
export class ChargingStation extends EventEmitter {
this.startHeartbeat()
}
- public restartMeterValues (connectorId: number, interval: number): void {
- this.stopMeterValues(connectorId)
- this.startMeterValues(connectorId, interval)
- }
-
public restartWebSocketPing (): void {
// Stop WebSocket ping
this.stopWebSocketPing()
}
}
- public startMeterValues (connectorId: number, interval: number): void {
- if (connectorId === 0) {
- logger.error(
- `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()}`
- )
- return
- }
- const connectorStatus = this.getConnectorStatus(connectorId)
- if (connectorStatus == null) {
- logger.error(
- `${this.logPrefix()} Trying to start MeterValues on non existing connector id
- ${connectorId.toString()}`
- )
- return
- }
- if (connectorStatus.transactionStarted === false) {
- logger.error(
- `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()} with no transaction started`
- )
- return
- } else if (
- connectorStatus.transactionStarted === true &&
- connectorStatus.transactionId == null
- ) {
- logger.error(
- `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()} with no transaction id`
- )
- return
- }
- if (interval > 0) {
- connectorStatus.transactionSetInterval = setInterval(() => {
- const transactionId = convertToInt(connectorStatus.transactionId)
- const meterValue = buildMeterValue(this, connectorId, transactionId, interval)
- this.ocppRequestService
- .requestHandler<MeterValuesRequest, MeterValuesResponse>(
- this,
- RequestCommand.METER_VALUES,
- {
- connectorId,
- meterValue: [meterValue],
- transactionId,
- } as MeterValuesRequest
- )
- .catch((error: unknown) => {
- logger.error(
- `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
- error
- )
- })
- }, clampToSafeTimerValue(interval))
- } else {
- logger.error(
- `${this.logPrefix()} Charging station ${
- StandardParametersKey.MeterValueSampleInterval
- } configuration set to ${interval.toString()}, not sending MeterValues`
- )
- }
- }
-
- public startTxUpdatedInterval (connectorId: number, interval: number): void {
- if (this.stationInfo?.ocppVersion !== OCPPVersion.VERSION_20) {
- return
- }
- const connector = this.getConnectorStatus(connectorId)
- if (connector == null) {
- logger.error(`${this.logPrefix()} Connector ${connectorId.toString()} not found`)
- return
- }
- if (interval <= 0) {
- logger.debug(
- `${this.logPrefix()} TxUpdatedInterval is ${interval.toString()}, not starting periodic TransactionEvent`
- )
- return
- }
- if (connector.transactionTxUpdatedSetInterval != null) {
- logger.warn(`${this.logPrefix()} TxUpdatedInterval already started, stopping first`)
- this.stopTxUpdatedInterval(connectorId)
- }
- connector.transactionTxUpdatedSetInterval = setInterval(() => {
- const connectorStatus = this.getConnectorStatus(connectorId)
- if (connectorStatus?.transactionStarted === true && connectorStatus.transactionId != null) {
- const meterValue = buildMeterValue(this, connectorId, 0, interval) as OCPP20MeterValue
- OCPP20ServiceUtils.sendTransactionEvent(
- this,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.MeterValuePeriodic,
- connectorId,
- connectorStatus.transactionId as string,
- { meterValue: [meterValue] }
- ).catch((error: unknown) => {
- logger.error(
- `${this.logPrefix()} Error sending periodic TransactionEvent at TxUpdatedInterval:`,
- error
- )
- })
- }
- }, clampToSafeTimerValue(interval))
- logger.info(
- `${this.logPrefix()} TxUpdatedInterval started every ${formatDurationMilliSeconds(interval)}`
- )
- }
-
public async stop (
reason?: StopTransactionReason,
stopTransactions = this.stationInfo?.stopTransactionsOnStopped
this.emitChargingStationEvent(ChargingStationEvents.updated)
}
- public stopMeterValues (connectorId: number): void {
- const connectorStatus = this.getConnectorStatus(connectorId)
- if (connectorStatus?.transactionSetInterval != null) {
- clearInterval(connectorStatus.transactionSetInterval)
- }
- }
-
- public async stopTransactionOnConnector (
- connectorId: number,
- reason?: StopTransactionReason
- ): Promise<StopTransactionResponse> {
- const rawTransactionId = this.getConnectorStatus(connectorId)?.transactionId
- const transactionId = rawTransactionId != null ? convertToInt(rawTransactionId) : undefined
- if (
- this.stationInfo?.beginEndMeterValues === true &&
- this.stationInfo.ocppStrictCompliance === true &&
- this.stationInfo.outOfOrderEndMeterValues === false
- ) {
- const transactionEndMeterValue = buildTransactionEndMeterValue(
- this,
- connectorId,
- this.getEnergyActiveImportRegisterByTransactionId(rawTransactionId)
- )
- await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
- this,
- RequestCommand.METER_VALUES,
- {
- connectorId,
- meterValue: [transactionEndMeterValue],
- transactionId,
- } as MeterValuesRequest
- )
- }
- return await this.ocppRequestService.requestHandler<
- Partial<StopTransactionRequest>,
- StopTransactionResponse
- >(this, RequestCommand.STOP_TRANSACTION, {
- meterStop: this.getEnergyActiveImportRegisterByTransactionId(rawTransactionId, true),
- transactionId,
- ...(reason != null && { reason: reason as StopTransactionRequest['reason'] }),
- })
- }
-
- public stopTxUpdatedInterval (connectorId: number): void {
- const connector = this.getConnectorStatus(connectorId)
- if (connector?.transactionTxUpdatedSetInterval != null) {
- clearInterval(connector.transactionTxUpdatedSetInterval)
- delete connector.transactionTxUpdatedSetInterval
- logger.info(`${this.logPrefix()} TxUpdatedInterval stopped`)
- }
- }
-
private add (): void {
this.emitChargingStationEvent(ChargingStationEvents.added)
}
}
}
- private async flushQueuedTransactionEvents (): Promise<void> {
- if (this.hasEvses) {
- for (const evseStatus of this.evses.values()) {
- for (const [connectorId, connectorStatus] of evseStatus.connectors) {
- if ((connectorStatus.transactionEventQueue?.length ?? 0) === 0) {
- continue
- }
- await OCPP20ServiceUtils.sendQueuedTransactionEvents(this, connectorId).catch(
- (error: unknown) => {
- logger.error(
- `${this.logPrefix()} Error while flushing queued TransactionEvents:`,
- error
- )
- }
- )
- }
- }
- } else {
- for (const [connectorId, connectorStatus] of this.connectors) {
- if ((connectorStatus.transactionEventQueue?.length ?? 0) === 0) {
- continue
- }
- await OCPP20ServiceUtils.sendQueuedTransactionEvents(this, connectorId).catch(
- (error: unknown) => {
- logger.error(
- `${this.logPrefix()} Error while flushing queued TransactionEvents:`,
- error
- )
- }
- )
- }
- }
- }
-
private getAmperageLimitation (): number | undefined {
if (
isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount.toString()}) or retry disabled (${this.stationInfo?.registrationMaxRetries?.toString()})`
)
- } else if (this.stationInfo?.ocppVersion === OCPPVersion.VERSION_20) {
- await this.flushQueuedTransactionEvents()
+ } else {
+ await flushQueuedTransactionMessages(this)
}
this.emitChargingStationEvent(ChargingStationEvents.updated)
} else {
): Promise<void> {
this.internalStopMessageSequence()
// Stop ongoing transactions
- stopTransactions && (await this.stopRunningTransactions(reason))
+ stopTransactions && (await stopRunningTransactions(this, reason))
if (this.hasEvses) {
for (const [evseId, evseStatus] of this.evses) {
if (evseId > 0) {
}
}
- private async stopRunningTransactions (reason?: StopTransactionReason): Promise<void> {
- if (
- this.stationInfo?.ocppVersion === OCPPVersion.VERSION_20 ||
- this.stationInfo?.ocppVersion === OCPPVersion.VERSION_201
- ) {
- await this.stopRunningTransactionsOCPP20(reason)
- return
- }
- if (this.hasEvses) {
- for (const [evseId, evseStatus] of this.evses) {
- if (evseId === 0) {
- continue
- }
- for (const [connectorId, connectorStatus] of evseStatus.connectors) {
- if (connectorStatus.transactionStarted === true) {
- await this.stopTransactionOnConnector(connectorId, reason)
- }
- }
- }
- } else {
- for (const connectorId of this.connectors.keys()) {
- if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
- await this.stopTransactionOnConnector(connectorId, reason)
- }
- }
- }
- }
-
- private async stopRunningTransactionsOCPP20 (reason?: StopTransactionReason): Promise<void> {
- const stoppedReason =
- reason != null ? (reason as unknown as OCPP20ReasonEnumType) : OCPP20ReasonEnumType.Local
- const terminationPromises: Promise<unknown>[] = []
-
- for (const [evseId, evseStatus] of this.evses) {
- if (evseId === 0) {
- continue
- }
- for (const [connectorId, connectorStatus] of evseStatus.connectors) {
- if (
- connectorStatus.transactionStarted === true ||
- connectorStatus.transactionPending === true
- ) {
- logger.info(
- `${this.logPrefix()} stopRunningTransactionsOCPP20: Stopping transaction ${connectorStatus.transactionId?.toString() ?? 'unknown'} on connector ${connectorId.toString()}`
- )
- terminationPromises.push(
- OCPP20ServiceUtils.requestStopTransaction(
- this,
- connectorId,
- evseId,
- OCPP20TriggerReasonEnumType.StopAuthorized,
- stoppedReason
- ).catch((error: unknown) => {
- logger.error(
- `${this.logPrefix()} stopRunningTransactionsOCPP20: Error stopping transaction on connector ${connectorId.toString()}:`,
- error
- )
- })
- )
- }
- }
- }
-
- if (terminationPromises.length > 0) {
- await Promise.all(terminationPromises)
- logger.info(
- `${this.logPrefix()} stopRunningTransactionsOCPP20: All transactions stopped on charging station`
- )
- }
- }
-
private stopWebSocketPing (): void {
if (this.wsPingSetInterval != null) {
clearInterval(this.wsPingSetInterval)
connectorId++
) {
if (chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
- chargingStation.restartMeterValues(
+ OCPP16ServiceUtils.stopPeriodicMeterValues(chargingStation, connectorId)
+ OCPP16ServiceUtils.startPeriodicMeterValues(
+ chargingStation,
connectorId,
secondsToMilliseconds(convertToInt(value))
)
return OCPP16Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED
}
if (chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
- const stopResponse = await chargingStation.stopTransactionOnConnector(
+ const stopResponse = await OCPP16ServiceUtils.stopTransactionOnConnector(
+ chargingStation,
connectorId,
OCPP16StopTransactionReason.UNLOCK_COMMAND
)
chargingStation,
OCPP16StandardParametersKey.MeterValueSampleInterval
)
- chargingStation.startMeterValues(
+ OCPP16ServiceUtils.startPeriodicMeterValues(
+ chargingStation,
connectorId,
configuredMeterValueSampleInterval != null
? secondsToMilliseconds(convertToInt(configuredMeterValueSampleInterval.value))
chargingStation.powerDivider!--
}
resetConnectorStatus(chargingStation.getConnectorStatus(transactionConnectorId))
- chargingStation.stopMeterValues(transactionConnectorId)
+ OCPP16ServiceUtils.stopPeriodicMeterValues(chargingStation, transactionConnectorId)
const logMsg = `${chargingStation.logPrefix()} ${moduleName}.handleResponseStopTransaction: Transaction with id ${requestPayload.transactionId.toString()} STOPPED on ${
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
chargingStation.stationInfo?.chargingStationId
chargingStation: ChargingStation,
connectorId: number
): Promise<void> {
- chargingStation.stopMeterValues(connectorId)
+ OCPP16ServiceUtils.stopPeriodicMeterValues(chargingStation, connectorId)
const connectorStatus = chargingStation.getConnectorStatus(connectorId)
resetConnectorStatus(connectorStatus)
await OCPP16ServiceUtils.restoreConnectorStatus(chargingStation, connectorId, connectorStatus)
import {
type ConfigurationKey,
type GenericResponse,
+ type MeterValuesRequest,
+ type MeterValuesResponse,
OCPP16AuthorizationStatus,
type OCPP16AvailabilityType,
type OCPP16ChangeAvailabilityResponse,
OCPP16StopTransactionReason,
type OCPP16SupportedFeatureProfiles,
OCPPVersion,
+ RequestCommand,
+ type StartTransactionRequest,
+ type StartTransactionResponse,
+ type StopTransactionReason,
+ type StopTransactionRequest,
+ type StopTransactionResponse,
} from '../../../types/index.js'
-import { convertToDate, isNotEmptyArray, logger, roundTo } from '../../../utils/index.js'
-import { OCPPServiceUtils } from '../OCPPServiceUtils.js'
+import {
+ clampToSafeTimerValue,
+ convertToDate,
+ convertToInt,
+ isNotEmptyArray,
+ logger,
+ roundTo,
+} from '../../../utils/index.js'
+import {
+ buildMeterValue,
+ buildTransactionEndMeterValue,
+ OCPPServiceUtils,
+} from '../OCPPServiceUtils.js'
import { OCPP16Constants } from './OCPP16Constants.js'
const moduleName = 'OCPP16ServiceUtils'
connectorId,
OCPP16ChargePointStatus.Finishing
)
- const stopResponse = await chargingStation.stopTransactionOnConnector(
+ const stopResponse = await OCPP16ServiceUtils.stopTransactionOnConnector(
+ chargingStation,
connectorId,
OCPP16StopTransactionReason.REMOTE
)
!cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp)
}
+ public static startPeriodicMeterValues (
+ chargingStation: ChargingStation,
+ connectorId: number,
+ interval: number
+ ): void {
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+ if (connectorStatus == null) {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.startPeriodicMeterValues: Connector ${connectorId.toString()} not found`
+ )
+ return
+ }
+ if (connectorStatus.transactionStarted !== true || connectorStatus.transactionId == null) {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.startPeriodicMeterValues: No active transaction on connector ${connectorId.toString()}`
+ )
+ return
+ }
+ if (interval <= 0) {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.startPeriodicMeterValues: MeterValueSampleInterval set to ${interval.toString()}, not sending MeterValues`
+ )
+ return
+ }
+ connectorStatus.transactionSetInterval = setInterval(() => {
+ const transactionId = convertToInt(connectorStatus.transactionId)
+ const meterValue = buildMeterValue(chargingStation, connectorId, transactionId, interval)
+ chargingStation.ocppRequestService
+ .requestHandler<MeterValuesRequest, MeterValuesResponse>(
+ chargingStation,
+ RequestCommand.METER_VALUES,
+ {
+ connectorId,
+ meterValue: [meterValue],
+ transactionId,
+ } as MeterValuesRequest
+ )
+ .catch((error: unknown) => {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.startPeriodicMeterValues: Error while sending '${RequestCommand.METER_VALUES}':`,
+ error
+ )
+ })
+ }, clampToSafeTimerValue(interval))
+ }
+
+ public static async startTransactionOnConnector (
+ chargingStation: ChargingStation,
+ connectorId: number,
+ idTag?: string
+ ): Promise<StartTransactionResponse> {
+ return chargingStation.ocppRequestService.requestHandler<
+ Partial<StartTransactionRequest>,
+ StartTransactionResponse
+ >(chargingStation, RequestCommand.START_TRANSACTION, {
+ connectorId,
+ ...(idTag != null && { idTag }),
+ })
+ }
+
+ public static stopPeriodicMeterValues (
+ chargingStation: ChargingStation,
+ connectorId: number
+ ): void {
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+ if (connectorStatus?.transactionSetInterval != null) {
+ clearInterval(connectorStatus.transactionSetInterval)
+ delete connectorStatus.transactionSetInterval
+ }
+ }
+
+ public static async stopTransactionOnConnector (
+ chargingStation: ChargingStation,
+ connectorId: number,
+ reason?: StopTransactionReason
+ ): Promise<StopTransactionResponse> {
+ const rawTransactionId = chargingStation.getConnectorStatus(connectorId)?.transactionId
+ const transactionId = rawTransactionId != null ? convertToInt(rawTransactionId) : undefined
+ if (
+ chargingStation.stationInfo?.beginEndMeterValues === true &&
+ chargingStation.stationInfo.ocppStrictCompliance === true &&
+ chargingStation.stationInfo.outOfOrderEndMeterValues === false
+ ) {
+ const transactionEndMeterValue = buildTransactionEndMeterValue(
+ chargingStation,
+ connectorId,
+ chargingStation.getEnergyActiveImportRegisterByTransactionId(rawTransactionId)
+ )
+ await chargingStation.ocppRequestService.requestHandler<
+ MeterValuesRequest,
+ MeterValuesResponse
+ >(chargingStation, RequestCommand.METER_VALUES, {
+ connectorId,
+ meterValue: [transactionEndMeterValue],
+ transactionId,
+ } as MeterValuesRequest)
+ }
+ return await chargingStation.ocppRequestService.requestHandler<
+ Partial<StopTransactionRequest>,
+ StopTransactionResponse
+ >(chargingStation, RequestCommand.STOP_TRANSACTION, {
+ meterStop: chargingStation.getEnergyActiveImportRegisterByTransactionId(
+ rawTransactionId,
+ true
+ ),
+ transactionId,
+ ...(reason != null && { reason: reason as StopTransactionRequest['reason'] }),
+ })
+ }
+
private static readonly composeChargingSchedule = (
chargingSchedule: OCPP16ChargingSchedule,
compositeInterval: Interval
connectorId: number,
evseId?: number
): Promise<void> {
- chargingStation.stopMeterValues(connectorId)
+ OCPP20ServiceUtils.stopPeriodicMeterValues(chargingStation, connectorId)
const connectorStatus = chargingStation.getConnectorStatus(connectorId)
resetConnectorStatus(connectorStatus)
await restoreConnectorStatus(chargingStation, connectorId, connectorStatus)
chargingStation: ChargingStation,
reason: OCPP20ReasonEnumType
): Promise<void> {
- const terminationPromises: Promise<unknown>[] = []
-
- for (const [evseId, evse] of chargingStation.evses) {
- for (const [connectorId, connector] of evse.connectors) {
- if (connector.transactionId != null) {
- logger.info(
- `${chargingStation.logPrefix()} ${moduleName}.terminateAllTransactions: Terminating transaction ${connector.transactionId.toString()} on connector ${connectorId.toString()}`
- )
- terminationPromises.push(
- OCPP20ServiceUtils.requestStopTransaction(chargingStation, connectorId, evseId).catch(
- (error: unknown) => {
- logger.error(
- `${chargingStation.logPrefix()} ${moduleName}.terminateAllTransactions: Error terminating transaction on connector ${connectorId.toString()}:`,
- error
- )
- }
- )
- )
- }
- }
- }
-
- if (terminationPromises.length > 0) {
- await Promise.all(terminationPromises)
- logger.info(
- `${chargingStation.logPrefix()} ${moduleName}.terminateAllTransactions: All transactions terminated on charging station`
- )
- }
+ await OCPP20ServiceUtils.stopAllTransactions(
+ chargingStation,
+ OCPP20TriggerReasonEnumType.ResetCommand,
+ reason
+ )
}
/**
evseId: number,
reason: OCPP20ReasonEnumType
): Promise<void> {
- const evse = chargingStation.getEvseStatus(evseId)
- if (!evse) {
- logger.warn(
- `${chargingStation.logPrefix()} ${moduleName}.terminateEvseTransactions: EVSE ${evseId.toString()} not found`
- )
- return
- }
-
- const terminationPromises: Promise<unknown>[] = []
- for (const [connectorId, connector] of evse.connectors) {
- if (connector.transactionId != null) {
- logger.info(
- `${chargingStation.logPrefix()} ${moduleName}.terminateEvseTransactions: Terminating transaction ${connector.transactionId.toString()} on connector ${connectorId.toString()}`
- )
- terminationPromises.push(
- OCPP20ServiceUtils.requestStopTransaction(chargingStation, connectorId, evseId).catch(
- (error: unknown) => {
- logger.error(
- `${chargingStation.logPrefix()} ${moduleName}.terminateEvseTransactions: Error terminating transaction on connector ${connectorId.toString()}:`,
- error
- )
- }
- )
- )
- }
- }
-
- if (terminationPromises.length > 0) {
- await Promise.all(terminationPromises)
- logger.info(
- `${chargingStation.logPrefix()} ${moduleName}.terminateEvseTransactions: All transactions terminated on EVSE ${evseId.toString()}`
- )
- }
+ await OCPP20ServiceUtils.stopAllTransactions(
+ chargingStation,
+ OCPP20TriggerReasonEnumType.ResetCommand,
+ reason,
+ evseId
+ )
}
private toHandler (
)
})
const txUpdatedInterval = OCPP20ServiceUtils.getTxUpdatedInterval(chargingStation)
- chargingStation.startTxUpdatedInterval(connectorId, txUpdatedInterval)
+ OCPP20ServiceUtils.startPeriodicMeterValues(
+ chargingStation,
+ connectorId,
+ txUpdatedInterval
+ )
}
logger.info(
`${chargingStation.logPrefix()} ${moduleName}.handleResponseTransactionEvent: Transaction ${requestPayload.transactionInfo.transactionId} STARTED on connector ${String(connectorId)}`
type UUIDv4,
} from '../../../types/index.js'
import {
+ clampToSafeTimerValue,
Constants,
convertToIntOrNaN,
+ formatDurationMilliSeconds,
generateUUID,
logger,
validateIdentifierString,
} from '../../../utils/index.js'
import { getConfigurationKey } from '../../ConfigurationKeyUtils.js'
-import { OCPPServiceUtils, sendAndSetConnectorStatus } from '../OCPPServiceUtils.js'
+import {
+ buildMeterValue,
+ OCPPServiceUtils,
+ sendAndSetConnectorStatus,
+} from '../OCPPServiceUtils.js'
import { OCPP20VariableManager } from './OCPP20VariableManager.js'
const moduleName = 'OCPP20ServiceUtils'
}
)
- chargingStation.stopTxUpdatedInterval(connectorId)
+ OCPP20ServiceUtils.stopPeriodicMeterValues(chargingStation, connectorId)
resetConnectorStatus(connectorStatus)
await sendAndSetConnectorStatus(chargingStation, connectorId, ConnectorStatusEnum.Available)
}
)
- chargingStation.stopTxUpdatedInterval(connectorId)
+ OCPP20ServiceUtils.stopPeriodicMeterValues(chargingStation, connectorId)
resetConnectorStatus(connectorStatus)
await sendAndSetConnectorStatus(chargingStation, connectorId, ConnectorStatusEnum.Available)
}
}
+ public static startPeriodicMeterValues (
+ chargingStation: ChargingStation,
+ connectorId: number,
+ interval: number
+ ): void {
+ const connector = chargingStation.getConnectorStatus(connectorId)
+ if (connector == null) {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.startPeriodicMeterValues: Connector ${connectorId.toString()} not found`
+ )
+ return
+ }
+ if (interval <= 0) {
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.startPeriodicMeterValues: TxUpdatedInterval is ${interval.toString()}, not starting periodic TransactionEvent`
+ )
+ return
+ }
+ if (connector.transactionTxUpdatedSetInterval != null) {
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.startPeriodicMeterValues: TxUpdatedInterval already started, stopping first`
+ )
+ OCPP20ServiceUtils.stopPeriodicMeterValues(chargingStation, connectorId)
+ }
+ connector.transactionTxUpdatedSetInterval = setInterval(() => {
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+ if (connectorStatus?.transactionStarted === true && connectorStatus.transactionId != null) {
+ const meterValue = buildMeterValue(
+ chargingStation,
+ connectorId,
+ 0,
+ interval
+ ) as OCPP20MeterValue
+ OCPP20ServiceUtils.sendTransactionEvent(
+ chargingStation,
+ OCPP20TransactionEventEnumType.Updated,
+ OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+ connectorId,
+ connectorStatus.transactionId as string,
+ { meterValue: [meterValue] }
+ ).catch((error: unknown) => {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.startPeriodicMeterValues: Error sending periodic TransactionEvent:`,
+ error
+ )
+ })
+ }
+ }, clampToSafeTimerValue(interval))
+ logger.info(
+ `${chargingStation.logPrefix()} ${moduleName}.startPeriodicMeterValues: TxUpdatedInterval started every ${formatDurationMilliSeconds(interval)}`
+ )
+ }
+
+ public static async stopAllTransactions (
+ chargingStation: ChargingStation,
+ triggerReason: OCPP20TriggerReasonEnumType = OCPP20TriggerReasonEnumType.RemoteStop,
+ stoppedReason: OCPP20ReasonEnumType = OCPP20ReasonEnumType.Remote,
+ evseId?: number
+ ): Promise<void> {
+ const terminationPromises: Promise<unknown>[] = []
+ if (evseId != null) {
+ const evseStatus = chargingStation.getEvseStatus(evseId)
+ if (evseStatus != null) {
+ for (const [connectorId, connectorStatus] of evseStatus.connectors) {
+ if (connectorStatus.transactionId != null) {
+ terminationPromises.push(
+ OCPP20ServiceUtils.requestStopTransaction(
+ chargingStation,
+ connectorId,
+ evseId,
+ triggerReason,
+ stoppedReason
+ ).catch((error: unknown) => {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.stopAllTransactions: Error stopping transaction on connector ${connectorId.toString()}:`,
+ error
+ )
+ })
+ )
+ }
+ }
+ }
+ } 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
+ )
+ })
+ )
+ }
+ }
+ }
+ }
+ if (terminationPromises.length > 0) {
+ await Promise.all(terminationPromises)
+ }
+ }
+
+ public static stopPeriodicMeterValues (
+ chargingStation: ChargingStation,
+ connectorId: number
+ ): void {
+ const connector = chargingStation.getConnectorStatus(connectorId)
+ if (connector?.transactionTxUpdatedSetInterval != null) {
+ clearInterval(connector.transactionTxUpdatedSetInterval)
+ delete connector.transactionTxUpdatedSetInterval
+ logger.info(
+ `${chargingStation.logPrefix()} ${moduleName}.stopPeriodicMeterValues: TxUpdatedInterval stopped`
+ )
+ }
+ }
+
private static buildFinalMeterValues (connectorStatus: ConnectorStatus): OCPP20MeterValue[] {
const finalMeterValues: OCPP20MeterValue[] = []
const energyValue = connectorStatus.transactionEnergyActiveImportRegisterValue ?? 0
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
+import type { StopTransactionReason } from '../../types/index.js'
+
import {
type ChargingStation,
getConfigurationKey,
type OCPP16MeterValue,
type OCPP16SampledValue,
type OCPP16StatusNotificationRequest,
+ OCPP16StopTransactionReason,
+ OCPP20AuthorizationStatusEnumType,
type OCPP20ConnectorStatusEnumType,
+ OCPP20IdTokenEnumType,
type OCPP20MeterValue,
+ OCPP20ReasonEnumType,
type OCPP20SampledValue,
+ OCPP20TransactionEventEnumType,
+ OCPP20TriggerReasonEnumType,
OCPPVersion,
RequestCommand,
type SampledValue,
type SampledValueTemplate,
StandardParametersKey,
+ type StartTransactionResult,
type StatusNotificationRequest,
type StatusNotificationResponse,
+ type StopTransactionResult,
} from '../../types/index.js'
import {
ACElectricUtils,
convertToFloat,
convertToInt,
DCElectricUtils,
+ generateUUID,
getRandomFloatFluctuatedRounded,
getRandomFloatRounded,
handleFileException,
}
}
+export const mapStopReasonToOCPP20 = (
+ reason?: StopTransactionReason
+): {
+ stoppedReason: OCPP20ReasonEnumType
+ triggerReason: OCPP20TriggerReasonEnumType
+} => {
+ switch (reason) {
+ case OCPP16StopTransactionReason.DE_AUTHORIZED:
+ case OCPP20ReasonEnumType.DeAuthorized:
+ return {
+ stoppedReason: OCPP20ReasonEnumType.DeAuthorized,
+ triggerReason: OCPP20TriggerReasonEnumType.Deauthorized,
+ }
+ case OCPP16StopTransactionReason.EMERGENCY_STOP:
+ case OCPP20ReasonEnumType.EmergencyStop:
+ return {
+ stoppedReason: OCPP20ReasonEnumType.EmergencyStop,
+ triggerReason: OCPP20TriggerReasonEnumType.AbnormalCondition,
+ }
+ case OCPP16StopTransactionReason.EV_DISCONNECTED:
+ case OCPP20ReasonEnumType.EVDisconnected:
+ return {
+ stoppedReason: OCPP20ReasonEnumType.EVDisconnected,
+ triggerReason: OCPP20TriggerReasonEnumType.EVDeparted,
+ }
+ case OCPP16StopTransactionReason.HARD_RESET:
+ case OCPP16StopTransactionReason.REBOOT:
+ case OCPP16StopTransactionReason.SOFT_RESET:
+ case OCPP20ReasonEnumType.ImmediateReset:
+ case OCPP20ReasonEnumType.Reboot:
+ return {
+ stoppedReason: OCPP20ReasonEnumType.ImmediateReset,
+ triggerReason: OCPP20TriggerReasonEnumType.ResetCommand,
+ }
+ case OCPP16StopTransactionReason.OTHER:
+ case OCPP20ReasonEnumType.Other:
+ return {
+ stoppedReason: OCPP20ReasonEnumType.Other,
+ triggerReason: OCPP20TriggerReasonEnumType.AbnormalCondition,
+ }
+ case OCPP16StopTransactionReason.POWER_LOSS:
+ case OCPP20ReasonEnumType.PowerLoss:
+ return {
+ stoppedReason: OCPP20ReasonEnumType.PowerLoss,
+ triggerReason: OCPP20TriggerReasonEnumType.AbnormalCondition,
+ }
+ case OCPP16StopTransactionReason.REMOTE:
+ case OCPP20ReasonEnumType.Remote:
+ return {
+ stoppedReason: OCPP20ReasonEnumType.Remote,
+ triggerReason: OCPP20TriggerReasonEnumType.RemoteStop,
+ }
+ case OCPP16StopTransactionReason.LOCAL:
+ case OCPP20ReasonEnumType.Local:
+ case undefined:
+ default:
+ return {
+ stoppedReason: OCPP20ReasonEnumType.Local,
+ triggerReason: OCPP20TriggerReasonEnumType.StopAuthorized,
+ }
+ }
+}
+
+export const startTransactionOnConnector = async (
+ chargingStation: ChargingStation,
+ connectorId: number,
+ idTag?: string
+): Promise<StartTransactionResult> => {
+ switch (chargingStation.stationInfo?.ocppVersion) {
+ case OCPPVersion.VERSION_16: {
+ const { OCPP16ServiceUtils } = await import('./1.6/OCPP16ServiceUtils.js')
+ const response = await OCPP16ServiceUtils.startTransactionOnConnector(
+ chargingStation,
+ connectorId,
+ idTag
+ )
+ return { accepted: response.idTagInfo.status === AuthorizationStatus.ACCEPTED }
+ }
+ case OCPPVersion.VERSION_20:
+ case OCPPVersion.VERSION_201: {
+ const { OCPP20ServiceUtils } = await import('./2.0/OCPP20ServiceUtils.js')
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+ let transactionId = connectorStatus?.transactionId as string | undefined
+ if (transactionId == null) {
+ transactionId = generateUUID()
+ if (connectorStatus != null) {
+ connectorStatus.transactionId = transactionId
+ }
+ OCPP20ServiceUtils.resetTransactionSequenceNumber(chargingStation, connectorId)
+ }
+ const response = await OCPP20ServiceUtils.sendTransactionEvent(
+ chargingStation,
+ OCPP20TransactionEventEnumType.Started,
+ OCPP20TriggerReasonEnumType.Authorized,
+ connectorId,
+ transactionId,
+ {
+ idToken:
+ idTag != null ? { idToken: idTag, type: OCPP20IdTokenEnumType.Central } : undefined,
+ }
+ )
+ return {
+ accepted:
+ response.idTokenInfo == null ||
+ response.idTokenInfo.status === OCPP20AuthorizationStatusEnumType.Accepted,
+ }
+ }
+ default:
+ throw new OCPPError(
+ ErrorType.INTERNAL_ERROR,
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ `startTransactionOnConnector: unsupported OCPP version ${chargingStation.stationInfo?.ocppVersion}`
+ )
+ }
+}
+
+export const stopTransactionOnConnector = async (
+ chargingStation: ChargingStation,
+ connectorId: number,
+ reason?: StopTransactionReason
+): Promise<StopTransactionResult> => {
+ switch (chargingStation.stationInfo?.ocppVersion) {
+ case OCPPVersion.VERSION_16: {
+ const { OCPP16ServiceUtils } = await import('./1.6/OCPP16ServiceUtils.js')
+ const response = await OCPP16ServiceUtils.stopTransactionOnConnector(
+ chargingStation,
+ connectorId,
+ reason
+ )
+ return { accepted: response.idTagInfo?.status === AuthorizationStatus.ACCEPTED }
+ }
+ case OCPPVersion.VERSION_20:
+ case OCPPVersion.VERSION_201: {
+ const { OCPP20ServiceUtils } = await import('./2.0/OCPP20ServiceUtils.js')
+ const evseId = chargingStation.getEvseIdByConnectorId(connectorId)
+ if (evseId == null) {
+ logger.warn(
+ `${chargingStation.logPrefix()} stopTransactionOnConnector: cannot resolve EVSE ID for connector ${connectorId.toString()}, skipping`
+ )
+ return { accepted: false }
+ }
+ const { stoppedReason, triggerReason } = mapStopReasonToOCPP20(reason)
+ const response = await OCPP20ServiceUtils.requestStopTransaction(
+ chargingStation,
+ connectorId,
+ evseId,
+ triggerReason,
+ stoppedReason
+ )
+ return {
+ accepted:
+ response.idTokenInfo == null ||
+ response.idTokenInfo.status === OCPP20AuthorizationStatusEnumType.Accepted,
+ }
+ }
+ default:
+ throw new OCPPError(
+ ErrorType.INTERNAL_ERROR,
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ `stopTransactionOnConnector: unsupported OCPP version ${chargingStation.stationInfo?.ocppVersion}`
+ )
+ }
+}
+
+export const stopRunningTransactions = async (
+ chargingStation: ChargingStation,
+ reason?: StopTransactionReason
+): Promise<void> => {
+ switch (chargingStation.stationInfo?.ocppVersion) {
+ 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
+ )
+ }
+ }
+ }
+ break
+ }
+ case OCPPVersion.VERSION_20:
+ case OCPPVersion.VERSION_201: {
+ const { OCPP20ServiceUtils } = await import('./2.0/OCPP20ServiceUtils.js')
+ const { stoppedReason, triggerReason } = mapStopReasonToOCPP20(reason)
+ await OCPP20ServiceUtils.stopAllTransactions(chargingStation, triggerReason, stoppedReason)
+ break
+ }
+ default:
+ logger.warn(
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ `${chargingStation.logPrefix()} stopRunningTransactions: unsupported OCPP version ${chargingStation.stationInfo?.ocppVersion}, no transactions stopped`
+ )
+ }
+}
+
+export const startPeriodicMeterValues = async (
+ chargingStation: ChargingStation,
+ connectorId: number,
+ interval: number
+): Promise<void> => {
+ switch (chargingStation.stationInfo?.ocppVersion) {
+ case OCPPVersion.VERSION_16: {
+ const { OCPP16ServiceUtils } = await import('./1.6/OCPP16ServiceUtils.js')
+ OCPP16ServiceUtils.startPeriodicMeterValues(chargingStation, connectorId, interval)
+ break
+ }
+ case OCPPVersion.VERSION_20:
+ case OCPPVersion.VERSION_201: {
+ const { OCPP20ServiceUtils } = await import('./2.0/OCPP20ServiceUtils.js')
+ OCPP20ServiceUtils.startPeriodicMeterValues(chargingStation, connectorId, interval)
+ break
+ }
+ default:
+ logger.error(
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ `${chargingStation.logPrefix()} OCPPServiceUtils.startPeriodicMeterValues: unsupported OCPP version ${chargingStation.stationInfo?.ocppVersion}`
+ )
+ }
+}
+
+export const stopPeriodicMeterValues = async (
+ chargingStation: ChargingStation,
+ connectorId: number
+): Promise<void> => {
+ switch (chargingStation.stationInfo?.ocppVersion) {
+ case OCPPVersion.VERSION_16: {
+ const { OCPP16ServiceUtils } = await import('./1.6/OCPP16ServiceUtils.js')
+ OCPP16ServiceUtils.stopPeriodicMeterValues(chargingStation, connectorId)
+ break
+ }
+ case OCPPVersion.VERSION_20:
+ case OCPPVersion.VERSION_201: {
+ const { OCPP20ServiceUtils } = await import('./2.0/OCPP20ServiceUtils.js')
+ OCPP20ServiceUtils.stopPeriodicMeterValues(chargingStation, connectorId)
+ break
+ }
+ default:
+ break
+ }
+}
+
+export const flushQueuedTransactionMessages = async (
+ chargingStation: ChargingStation
+): Promise<void> => {
+ switch (chargingStation.stationInfo?.ocppVersion) {
+ case OCPPVersion.VERSION_16:
+ break
+ 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) => {
+ logger.error(
+ `${chargingStation.logPrefix()} OCPPServiceUtils.flushQueuedTransactionMessages: Error flushing queued TransactionEvents:`,
+ error
+ )
+ })
+ }
+ }
+ }
+ break
+ }
+ default:
+ break
+ }
+}
+
const checkConnectorStatusTransition = async (
chargingStation: ChargingStation,
connectorId: number,
public static readonly buildTransactionEndMeterValue = buildTransactionEndMeterValue
public static readonly isIdTagAuthorized = isIdTagAuthorized
public static readonly isIdTagAuthorizedUnified = isIdTagAuthorizedUnified
+ public static readonly mapStopReasonToOCPP20 = mapStopReasonToOCPP20
public static readonly restoreConnectorStatus = restoreConnectorStatus
public static readonly sendAndSetConnectorStatus = sendAndSetConnectorStatus
+ public static readonly stopRunningTransactions = stopRunningTransactions
protected static buildSampledValue = buildSampledValue
protected static getSampledValueTemplate = getSampledValueTemplate
export { OCPP16IncomingRequestService } from './1.6/OCPP16IncomingRequestService.js'
export { OCPP16RequestService } from './1.6/OCPP16RequestService.js'
export { OCPP16ResponseService } from './1.6/OCPP16ResponseService.js'
+export { OCPP16ServiceUtils } from './1.6/OCPP16ServiceUtils.js'
export { OCPP20IncomingRequestService } from './2.0/OCPP20IncomingRequestService.js'
export { OCPP20RequestService } from './2.0/OCPP20RequestService.js'
export { OCPP20ResponseService } from './2.0/OCPP20ResponseService.js'
type AuthorizeResponse,
type StartTransactionRequest,
type StartTransactionResponse,
+ type StartTransactionResult,
StopTransactionReason,
type StopTransactionRequest,
type StopTransactionResponse,
+ type StopTransactionResult,
} from './ocpp/Transaction.js'
export { PerformanceRecord } from './orm/entities/PerformanceRecord.js'
export type { SimulatorState } from './SimulatorState.js'
...OCPP16StopTransactionReason,
...OCPP20ReasonEnumType,
} as const
+export interface StartTransactionResult {
+ readonly accepted: boolean
+}
+
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type StopTransactionReason = OCPP16StopTransactionReason | OCPP20ReasonEnumType
export type StopTransactionRequest = OCPP16StopTransactionRequest
export type StopTransactionResponse = OCPP16StopTransactionResponse
+
+export interface StopTransactionResult {
+ readonly accepted: boolean
+}
* - Singleton pattern (getInstance / deleteInstance)
* - Lifecycle state machine (start / stop / starting / stopping guards)
* - Connector status management (startConnector / stopConnector)
- * - handleStartTransactionResponse — transaction counter updates
+ * - handleStartTransactionResult — transaction counter updates
* - initializeConnectorsStatus — connector status initialization
*
* Note: The async transaction loop (internalStartConnector, startTransaction, stopTransaction)
import { AutomaticTransactionGenerator } from '../../src/charging-station/AutomaticTransactionGenerator.js'
import { BaseError } from '../../src/exception/index.js'
-import { AuthorizationStatus, type StartTransactionResponse } from '../../src/types/index.js'
+import { type StartTransactionResult } from '../../src/types/index.js'
import { flushMicrotasks } from '../helpers/TestLifecycleHelpers.js'
import { createMockChargingStation, standardCleanup } from './ChargingStationTestUtils.js'
}
/**
- * Extracts the private handleStartTransactionResponse method from an ATG instance.
+ * Extracts the private handleStartTransactionResult method from an ATG instance.
* @param atg - The ATG instance to extract the method from
- * @returns The bound handleStartTransactionResponse method
+ * @returns The bound handleStartTransactionResult method
*/
-function getHandleStartTransactionResponse (
+function getHandleStartTransactionResult (
atg: AutomaticTransactionGenerator
-): (connectorId: number, response: StartTransactionResponse) => void {
+): (connectorId: number, result: StartTransactionResult) => void {
return (
atg as unknown as {
- handleStartTransactionResponse: (
- connectorId: number,
- response: StartTransactionResponse
- ) => void
+ handleStartTransactionResult: (connectorId: number, result: StartTransactionResult) => void
}
- ).handleStartTransactionResponse.bind(atg)
+ ).handleStartTransactionResult.bind(atg)
}
/**
})
})
- await describe('handleStartTransactionResponse', async () => {
+ await describe('handleStartTransactionResult', async () => {
await it('should increment accepted counters on accepted start response', () => {
const station = createStationForATG()
const atg = getDefinedATG(station)
const connectorStatus = getConnectorStatus(atg, 1)
- const handleResponse = getHandleStartTransactionResponse(atg)
+ const handleResult = getHandleStartTransactionResult(atg)
- handleResponse(1, {
- idTagInfo: { status: AuthorizationStatus.ACCEPTED },
- transactionId: 1,
- } as StartTransactionResponse)
+ handleResult(1, { accepted: true })
assert.strictEqual(connectorStatus.startTransactionRequests, 1)
assert.strictEqual(connectorStatus.acceptedStartTransactionRequests, 1)
const station = createStationForATG()
const atg = getDefinedATG(station)
const connectorStatus = getConnectorStatus(atg, 1)
- const handleResponse = getHandleStartTransactionResponse(atg)
+ const handleResult = getHandleStartTransactionResult(atg)
- handleResponse(1, {
- idTagInfo: { status: AuthorizationStatus.INVALID },
- transactionId: 1,
- } as StartTransactionResponse)
+ handleResult(1, { accepted: false })
assert.strictEqual(connectorStatus.startTransactionRequests, 1)
assert.strictEqual(connectorStatus.acceptedStartTransactionRequests, 0)
+++ /dev/null
-/**
- * @file Tests for ChargingStation stopRunningTransactions
- * @description Verifies version-aware transaction stopping: OCPP 2.0 uses TransactionEvent(Ended),
- * OCPP 1.6 uses StopTransaction
- */
-import assert from 'node:assert/strict'
-import { afterEach, beforeEach, describe, it, mock } from 'node:test'
-
-import type { ChargingStation } from '../../src/charging-station/index.js'
-import type { EmptyObject, JsonType, StopTransactionReason } from '../../src/types/index.js'
-
-import { ChargingStation as ChargingStationClass } from '../../src/charging-station/ChargingStation.js'
-import { OCPPVersion } from '../../src/types/index.js'
-import { Constants } from '../../src/utils/index.js'
-import { setupConnectorWithTransaction, standardCleanup } from '../helpers/TestLifecycleHelpers.js'
-import { TEST_CHARGING_STATION_BASE_NAME } from './ChargingStationTestConstants.js'
-import { cleanupChargingStation, createMockChargingStation } from './ChargingStationTestUtils.js'
-
-interface TestableChargingStationPrivate {
- stopRunningTransactions: (reason?: StopTransactionReason) => Promise<void>
- stopRunningTransactionsOCPP20: (reason?: StopTransactionReason) => Promise<void>
- stopTransactionOnConnector: (
- connectorId: number,
- reason?: StopTransactionReason
- ) => Promise<unknown>
-}
-
-/**
- * Binds private ChargingStation methods to a mock station instance for testing
- * @param station - The mock station to bind methods to
- */
-function bindPrivateMethods (station: ChargingStation): void {
- const proto = ChargingStationClass.prototype as unknown as TestableChargingStationPrivate
- const stationRecord = station as unknown as Record<string, unknown>
- stationRecord.stopRunningTransactions = proto.stopRunningTransactions
- stationRecord.stopRunningTransactionsOCPP20 = proto.stopRunningTransactionsOCPP20
- stationRecord.stopTransactionOnConnector = proto.stopTransactionOnConnector
-}
-
-await describe('ChargingStation stopRunningTransactions', async () => {
- let station: ChargingStation | undefined
-
- beforeEach(() => {
- station = undefined
- })
-
- afterEach(() => {
- standardCleanup()
- if (station != null) {
- cleanupChargingStation(station)
- }
- })
-
- await it('should send TransactionEvent(Ended) for OCPP 2.0 stations when stopping running transactions', async () => {
- // Arrange
- const sentRequests: { command: string; payload: Record<string, unknown> }[] = []
- const requestHandlerMock = mock.fn(async (...args: unknown[]) => {
- sentRequests.push({
- command: args[1] as string,
- payload: args[2] as Record<string, unknown>,
- })
- return Promise.resolve({} as EmptyObject)
- })
-
- const result = createMockChargingStation({
- baseName: TEST_CHARGING_STATION_BASE_NAME,
- connectorsCount: 2,
- evseConfiguration: { evsesCount: 2 },
- heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
- ocppRequestService: {
- requestHandler: requestHandlerMock,
- },
- stationInfo: {
- ocppVersion: OCPPVersion.VERSION_201,
- },
- websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
- })
- station = result.station
- station.isWebSocketConnectionOpened = () => true
- bindPrivateMethods(station)
-
- setupConnectorWithTransaction(station, 1, { transactionId: 1001 })
- const connector1 = station.getConnectorStatus(1)
- if (connector1 != null) {
- connector1.transactionId = 'tx-ocpp20-1001'
- }
-
- // Act
- const testable = station as unknown as TestableChargingStationPrivate
- await testable.stopRunningTransactions()
-
- // Assert
- const transactionEventCalls = sentRequests.filter(r => r.command === 'TransactionEvent')
- assert.ok(transactionEventCalls.length > 0, 'Expected at least one TransactionEvent request')
- const stopTransactionCalls = sentRequests.filter(r => r.command === 'StopTransaction')
- assert.strictEqual(
- stopTransactionCalls.length,
- 0,
- 'Should not send StopTransaction for OCPP 2.0'
- )
- })
-
- await it('should send StopTransaction for OCPP 1.6 stations when stopping running transactions', async () => {
- // Arrange
- const sentRequests: { command: string; payload: Record<string, unknown> }[] = []
- const requestHandlerMock = mock.fn(async (...args: unknown[]) => {
- sentRequests.push({
- command: args[1] as string,
- payload: args[2] as Record<string, unknown>,
- })
- return Promise.resolve({ idTagInfo: { status: 'Accepted' } } as unknown as JsonType)
- })
-
- const result = createMockChargingStation({
- baseName: TEST_CHARGING_STATION_BASE_NAME,
- connectorsCount: 2,
- ocppRequestService: {
- requestHandler: requestHandlerMock,
- },
- stationInfo: {
- ocppVersion: OCPPVersion.VERSION_16,
- },
- })
- station = result.station
- station.isWebSocketConnectionOpened = () => true
- bindPrivateMethods(station)
-
- setupConnectorWithTransaction(station, 1, { transactionId: 5001 })
-
- // Act
- const testable = station as unknown as TestableChargingStationPrivate
- await testable.stopRunningTransactions()
-
- // Assert
- const stopTransactionCalls = sentRequests.filter(r => r.command === 'StopTransaction')
- assert.ok(stopTransactionCalls.length > 0, 'Expected at least one StopTransaction request')
- const transactionEventCalls = sentRequests.filter(r => r.command === 'TransactionEvent')
- assert.strictEqual(
- transactionEventCalls.length,
- 0,
- 'Should not send TransactionEvent for OCPP 1.6'
- )
- })
-
- await it('should handle errors gracefully when OCPP 2.0 transaction stop fails', async () => {
- // Arrange
- const requestHandlerMock = mock.fn(async () => {
- return Promise.reject(new Error('Simulated network error'))
- })
-
- const result = createMockChargingStation({
- baseName: TEST_CHARGING_STATION_BASE_NAME,
- connectorsCount: 2,
- evseConfiguration: { evsesCount: 2 },
- heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
- ocppRequestService: {
- requestHandler: requestHandlerMock,
- },
- stationInfo: {
- ocppVersion: OCPPVersion.VERSION_201,
- },
- websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
- })
- station = result.station
- station.isWebSocketConnectionOpened = () => true
- bindPrivateMethods(station)
-
- setupConnectorWithTransaction(station, 1, { transactionId: 2001 })
- const connector1 = station.getConnectorStatus(1)
- if (connector1 != null) {
- connector1.transactionId = 'tx-ocpp20-2001'
- }
-
- // Act & Assert — should not throw
- const testable = station as unknown as TestableChargingStationPrivate
- await testable.stopRunningTransactions()
- })
-
- await it('should also stop pending transactions for OCPP 2.0 stations', async () => {
- // Arrange
- const sentRequests: { command: string; payload: Record<string, unknown> }[] = []
- const requestHandlerMock = mock.fn(async (...args: unknown[]) => {
- sentRequests.push({
- command: args[1] as string,
- payload: args[2] as Record<string, unknown>,
- })
- return Promise.resolve({} as EmptyObject)
- })
-
- const result = createMockChargingStation({
- baseName: TEST_CHARGING_STATION_BASE_NAME,
- connectorsCount: 2,
- evseConfiguration: { evsesCount: 2 },
- heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
- ocppRequestService: {
- requestHandler: requestHandlerMock,
- },
- stationInfo: {
- ocppVersion: OCPPVersion.VERSION_201,
- },
- websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
- })
- station = result.station
- station.isWebSocketConnectionOpened = () => true
- bindPrivateMethods(station)
-
- // Set up a pending transaction (not started, but pending)
- const connector1 = station.getConnectorStatus(1)
- if (connector1 != null) {
- connector1.transactionPending = true
- connector1.transactionStarted = false
- connector1.transactionId = 'tx-pending-3001'
- }
-
- // Act
- const testable = station as unknown as TestableChargingStationPrivate
- await testable.stopRunningTransactions()
-
- // Assert
- const transactionEventCalls = sentRequests.filter(r => r.command === 'TransactionEvent')
- assert.ok(transactionEventCalls.length > 0, 'Expected TransactionEvent for pending transaction')
- })
-})
import type { ChargingStation } from '../../src/charging-station/index.js'
+import { OCPP16ServiceUtils } from '../../src/charging-station/ocpp/1.6/OCPP16ServiceUtils.js'
+import { OCPP20ServiceUtils } from '../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js'
import { OCPPVersion } from '../../src/types/index.js'
import { standardCleanup, withMockTimers } from '../helpers/TestLifecycleHelpers.js'
import { TEST_HEARTBEAT_INTERVAL_MS, TEST_ID_TAG } from './ChargingStationTestConstants.js'
}
// Act
- station.startMeterValues(1, 10000)
+ OCPP16ServiceUtils.startPeriodicMeterValues(station, 1, 10000)
// Assert - meter values interval should be created
if (connector1 != null) {
connector1.transactionStarted = true
connector1.transactionId = 100
}
- station.startMeterValues(1, 10000)
+ OCPP16ServiceUtils.startPeriodicMeterValues(station, 1, 10000)
const firstInterval = connector1?.transactionSetInterval
// Act
- station.restartMeterValues(1, 15000)
+ OCPP16ServiceUtils.stopPeriodicMeterValues(station, 1)
+ OCPP16ServiceUtils.startPeriodicMeterValues(station, 1, 15000)
const secondInterval = connector1?.transactionSetInterval
// Assert - interval should be different
connector1.transactionStarted = true
connector1.transactionId = 100
}
- station.startMeterValues(1, 10000)
+ OCPP16ServiceUtils.startPeriodicMeterValues(station, 1, 10000)
// Act
- station.stopMeterValues(1)
+ OCPP16ServiceUtils.stopPeriodicMeterValues(station, 1)
// Assert - interval should be cleared
assert.strictEqual(connector1?.transactionSetInterval, undefined)
}
// Act
- station.startTxUpdatedInterval(1, 5000)
+ OCPP20ServiceUtils.startPeriodicMeterValues(station, 1, 5000)
// Assert - transaction updated interval should be created
if (connector1 != null) {
connector1.transactionStarted = true
connector1.transactionId = 100
}
- station.startTxUpdatedInterval(1, 5000)
+ OCPP20ServiceUtils.startPeriodicMeterValues(station, 1, 5000)
// Act
- station.stopTxUpdatedInterval(1)
+ OCPP20ServiceUtils.stopPeriodicMeterValues(station, 1)
// Assert - interval should be cleared
assert.strictEqual(connector1?.transactionTxUpdatedSetInterval, undefined)
this.startHeartbeat()
},
- restartMeterValues (connectorId: number, interval: number): void {
- this.stopMeterValues(connectorId)
- this.startMeterValues(connectorId, interval)
- },
-
restartWebSocketPing (): void {
/* empty */
},
},
starting,
- startMeterValues (connectorId: number, interval: number): void {
- const connector = this.getConnectorStatus(connectorId)
- if (connector != null) {
- connector.transactionSetInterval = setInterval(() => {
- /* empty */
- }, interval)
- }
- },
-
- startTxUpdatedInterval (connectorId: number, interval: number): void {
- if (
- this.stationInfo.ocppVersion === OCPPVersion.VERSION_20 ||
- this.stationInfo.ocppVersion === OCPPVersion.VERSION_201
- ) {
- const connector = this.getConnectorStatus(connectorId)
- if (connector != null) {
- connector.transactionTxUpdatedSetInterval = setInterval(() => {
- /* empty */
- }, interval)
- }
- }
- },
-
startWebSocketPing (): void {
/* empty */
},
delete this.heartbeatSetInterval
}
},
- stopMeterValues (connectorId: number): void {
- const connector = this.getConnectorStatus(connectorId)
- if (connector?.transactionSetInterval != null) {
- clearInterval(connector.transactionSetInterval)
- delete connector.transactionSetInterval
- }
- },
stopping: false,
- stopTxUpdatedInterval (connectorId: number): void {
- const connector = this.getConnectorStatus(connectorId)
- if (connector?.transactionTxUpdatedSetInterval != null) {
- clearInterval(connector.transactionTxUpdatedSetInterval)
- delete connector.transactionTxUpdatedSetInterval
- }
- },
templateFile,
wsConnection: null as MockWebSocket | null,
wsConnectionRetryCount: 0,
Promise.resolve({ idTagInfo: { status: OCPP16AuthorizationStatus.ACCEPTED } })
)
- // Mock stopTransactionOnConnector — called by UnlockConnector when transaction is active
- station.stopTransactionOnConnector = async () =>
+ // Mock OCPP16ServiceUtils.stopTransactionOnConnector — called by UnlockConnector when transaction is active
+ mock.method(OCPP16ServiceUtils, 'stopTransactionOnConnector', async () =>
Promise.resolve({ idTagInfo: { status: OCPP16AuthorizationStatus.ACCEPTED } })
+ )
})
afterEach(() => {
await it('should return UnlockFailed when active transaction stop is rejected', async () => {
// Arrange
setupConnectorWithTransaction(station, 1, { transactionId: 200 })
- station.stopTransactionOnConnector = async () =>
+ mock.method(OCPP16ServiceUtils, 'stopTransactionOnConnector', async () =>
Promise.resolve({ idTagInfo: { status: OCPP16AuthorizationStatus.INVALID } })
+ )
// Act
const response = await testableService.handleRequestUnlockConnector(station, {
*/
import assert from 'node:assert/strict'
+import { mock } from 'node:test'
import { afterEach, beforeEach, describe, it } from 'node:test'
import type { ChargingStation } from '../../../../src/charging-station/index.js'
import { createTestableIncomingRequestService } from '../../../../src/charging-station/ocpp/1.6/__testable__/index.js'
import { OCPP16IncomingRequestService } from '../../../../src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.js'
import { OCPP16ResponseService as OCPP16ResponseServiceClass } from '../../../../src/charging-station/ocpp/1.6/OCPP16ResponseService.js'
+import { OCPP16ServiceUtils } from '../../../../src/charging-station/ocpp/1.6/OCPP16ServiceUtils.js'
import {
AvailabilityType,
GenericStatus,
const responseService = new OCPP16ResponseServiceClass()
// Mock meter value start/stop to avoid real timer setup
- station.startMeterValues = (_connectorId: number, _interval: number) => {
- /* noop */
- }
- station.stopMeterValues = (_connectorId: number) => {
- /* noop */
- }
+ mock.method(
+ OCPP16ServiceUtils,
+ 'startPeriodicMeterValues',
+ (_station: unknown, _connectorId: number, _interval: number) => {
+ /* noop */
+ }
+ )
+ mock.method(
+ OCPP16ServiceUtils,
+ 'stopPeriodicMeterValues',
+ (_station: unknown, _connectorId: number) => {
+ /* noop */
+ }
+ )
// Add MeterValues template required by buildTransactionBeginMeterValue
for (const [connectorId] of station.connectors) {
*/
import assert from 'node:assert/strict'
-import { afterEach, beforeEach, describe, it } from 'node:test'
+import { afterEach, beforeEach, describe, it, mock } from 'node:test'
import type { ChargingStation } from '../../../../src/charging-station/index.js'
import type { OCPP16ResponseService } from '../../../../src/charging-station/ocpp/1.6/OCPP16ResponseService.js'
OCPP16StopTransactionResponse,
} from '../../../../src/types/index.js'
+import { OCPP16ServiceUtils } from '../../../../src/charging-station/ocpp/1.6/OCPP16ServiceUtils.js'
import {
OCPP16AuthorizationStatus,
OCPP16MeterValueUnit,
setMockRequestHandler(station, async () => Promise.resolve({}))
// Mock startMeterValues/stopMeterValues to avoid real timer setup
- station.startMeterValues = (_connectorId: number, _interval: number) => {
+ mock.method(OCPP16ServiceUtils, 'startPeriodicMeterValues', () => {
/* noop */
- }
- station.stopMeterValues = (_connectorId: number) => {
+ })
+ mock.method(OCPP16ServiceUtils, 'stopPeriodicMeterValues', () => {
/* noop */
- }
+ })
// Add MeterValues template required by buildTransactionBeginMeterValue
for (const [connectorId] of station.connectors) {
OCPP20ServiceUtils,
} from '../../../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js'
import { OCPP20VariableManager } from '../../../../src/charging-station/ocpp/2.0/OCPP20VariableManager.js'
+import { startPeriodicMeterValues } from '../../../../src/charging-station/ocpp/OCPPServiceUtils.js'
import { OCPPError } from '../../../../src/exception/index.js'
import {
AttributeEnumType,
OCPPVersion,
} from '../../../../src/types/index.js'
import { Constants, generateUUID } from '../../../../src/utils/index.js'
-import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import { standardCleanup, withMockTimers } from '../../../helpers/TestLifecycleHelpers.js'
import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
import {
standardCleanup()
})
- await describe('startTxUpdatedInterval', async () => {
- await it('should not start timer for non-OCPP 2.0 stations', () => {
- const { station: ocpp16Station } = createMockChargingStation({
- baseName: TEST_CHARGING_STATION_BASE_NAME,
- connectorsCount: 1,
- stationInfo: {
- ocppVersion: OCPPVersion.VERSION_16,
- },
- })
+ await describe('startPeriodicMeterValues', async () => {
+ await it('should not start OCPP 2.0 timer for OCPP 1.6 stations via unified dispatch', async t => {
+ await withMockTimers(t, ['setInterval'], async () => {
+ const { station: ocpp16Station } = createMockChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 1,
+ stationInfo: {
+ ocppVersion: OCPPVersion.VERSION_16,
+ },
+ })
- // Call startTxUpdatedInterval on OCPP 1.6 station
- ocpp16Station.startTxUpdatedInterval(1, 60000)
+ await startPeriodicMeterValues(ocpp16Station, 1, 60000)
- // Verify no timer was started (method should return early)
- const connector = ocpp16Station.getConnectorStatus(1)
- assert.strictEqual(connector?.transactionTxUpdatedSetInterval, undefined)
+ const connector = ocpp16Station.getConnectorStatus(1)
+ assert.strictEqual(connector?.transactionTxUpdatedSetInterval, undefined)
+ })
})
await it('should not start timer when interval is zero', () => {
--- /dev/null
+/**
+ * @file Tests for OCPPServiceUtils stop transaction functions
+ * @description Verifies stopTransactionOnConnector and stopRunningTransactions
+ * version-dispatching functions
+ */
+
+import assert from 'node:assert/strict'
+import { afterEach, describe, it, mock } from 'node:test'
+
+import type { ChargingStation } from '../../../src/charging-station/index.js'
+import type { MockChargingStationOptions } from '../helpers/StationHelpers.js'
+
+import {
+ flushQueuedTransactionMessages,
+ mapStopReasonToOCPP20,
+ startTransactionOnConnector,
+ stopRunningTransactions,
+ stopTransactionOnConnector,
+} from '../../../src/charging-station/ocpp/OCPPServiceUtils.js'
+import {
+ type OCPP20TransactionEventRequest,
+ OCPPVersion,
+ type StopTransactionReason,
+} from '../../../src/types/index.js'
+import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
+import { createMockChargingStation } from '../ChargingStationTestUtils.js'
+
+/**
+ * Creates a mock charging station with a tracked request handler for testing
+ * @param opts - optional charging station configuration options
+ * @returns object with the mock station and the mock request handler function
+ */
+function createStationWithRequestHandler (opts?: Partial<MockChargingStationOptions>): {
+ requestHandler: ReturnType<typeof mock.fn>
+ station: ChargingStation
+} {
+ const requestHandler = mock.fn(async (..._args: unknown[]) => Promise.resolve({}))
+ const { station } = createMockChargingStation({
+ ocppRequestService: { requestHandler },
+ ...opts,
+ })
+ return { requestHandler, station }
+}
+
+/**
+ * Configures a connector with a pending (not started) transaction for testing
+ * @param station - the charging station mock
+ * @param connectorId - the connector ID to configure
+ * @param txId - the transaction ID to assign
+ */
+function setupPendingTransaction (
+ station: ChargingStation,
+ connectorId: number,
+ txId: string
+): void {
+ const connector = station.getConnectorStatus(connectorId)
+ if (connector == null) {
+ throw new Error(`Connector ${String(connectorId)} not found`)
+ }
+ connector.transactionPending = true
+ connector.transactionStarted = false
+ connector.transactionId = txId
+ connector.transactionStart = new Date()
+}
+
+/**
+ * Configures a connector with a started transaction for testing
+ * @param station - the charging station mock
+ * @param connectorId - the connector ID to configure
+ * @param txId - the transaction ID to assign
+ */
+function setupTransaction (
+ station: ChargingStation,
+ connectorId: number,
+ txId: number | string
+): void {
+ const connector = station.getConnectorStatus(connectorId)
+ if (connector == null) {
+ throw new Error(`Connector ${String(connectorId)} not found`)
+ }
+ connector.transactionStarted = true
+ connector.transactionId = txId
+ connector.transactionIdTag = `TAG-${String(txId)}`
+ connector.transactionStart = new Date()
+ connector.idTagAuthorized = true
+}
+
+await describe('OCPPServiceUtils — stop transaction functions', async () => {
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ await describe('stopTransactionOnConnector', async () => {
+ await it('should send StopTransaction for OCPP 1.6 stations and return accepted: true', async () => {
+ const { requestHandler, station } = createStationWithRequestHandler()
+ requestHandler.mock.mockImplementation(async (..._args: unknown[]) =>
+ Promise.resolve({ idTagInfo: { status: 'Accepted' } })
+ )
+ setupTransaction(station, 1, 100)
+
+ const result = await stopTransactionOnConnector(station, 1)
+
+ assert.strictEqual(result.accepted, true)
+ assert.ok(requestHandler.mock.calls.length >= 1)
+ assert.strictEqual(requestHandler.mock.calls[0].arguments[1] as string, 'StopTransaction')
+ })
+
+ await it('should send TransactionEvent(Ended) for OCPP 2.0 stations and return accepted: true', async () => {
+ const { requestHandler, station } = createStationWithRequestHandler({
+ evseConfiguration: { evsesCount: 1 },
+ ocppVersion: OCPPVersion.VERSION_20,
+ })
+ requestHandler.mock.mockImplementation(async (..._args: unknown[]) =>
+ Promise.resolve({ idTokenInfo: { status: 'Accepted' } })
+ )
+ setupTransaction(station, 1, 'tx-uuid-001')
+
+ const result = await stopTransactionOnConnector(station, 1)
+
+ assert.strictEqual(result.accepted, true)
+ assert.ok(requestHandler.mock.calls.length >= 1)
+ assert.strictEqual(requestHandler.mock.calls[0].arguments[1] as string, 'TransactionEvent')
+ })
+
+ await it('should throw OCPPError for unsupported OCPP version in stopTransactionOnConnector', async () => {
+ const { station } = createStationWithRequestHandler()
+ const stationInfo = station.stationInfo
+ if (stationInfo != null) {
+ ;(stationInfo as Record<string, unknown>).ocppVersion = '0.9'
+ }
+
+ await assert.rejects(
+ () => stopTransactionOnConnector(station, 1),
+ (error: Error) => {
+ assert.ok(error.message.includes('unsupported OCPP version'))
+ return true
+ }
+ )
+ })
+ })
+
+ await describe('stopRunningTransactions', async () => {
+ await it('should call stopTransactionOnConnector sequentially for each OCPP 1.6 connector with active transaction', async () => {
+ const sentCommands: string[] = []
+ const { requestHandler, station } = createStationWithRequestHandler({
+ connectorsCount: 2,
+ })
+ requestHandler.mock.mockImplementation(async (...args: unknown[]) => {
+ sentCommands.push(args[1] as string)
+ return Promise.resolve({ idTagInfo: { status: 'Accepted' } })
+ })
+ setupTransaction(station, 1, 101)
+ setupTransaction(station, 2, 102)
+
+ await stopRunningTransactions(station)
+
+ const stopCalls = sentCommands.filter(cmd => cmd === 'StopTransaction')
+ assert.strictEqual(stopCalls.length, 2)
+ })
+
+ await it('should call requestStopTransaction in parallel for OCPP 2.0 connectors', async () => {
+ const sentPayloads: { command: string; transactionId?: string }[] = []
+ const { requestHandler, station } = createStationWithRequestHandler({
+ connectorsCount: 2,
+ evseConfiguration: { evsesCount: 2 },
+ ocppVersion: OCPPVersion.VERSION_20,
+ })
+ requestHandler.mock.mockImplementation(async (...args: unknown[]) => {
+ const payload = args[2] as Record<string, unknown>
+ sentPayloads.push({
+ command: args[1] as string,
+ transactionId: payload.transactionId as string | undefined,
+ })
+ return Promise.resolve({ idTokenInfo: { status: 'Accepted' } })
+ })
+ setupTransaction(station, 1, 'tx-001')
+ setupTransaction(station, 2, 'tx-002')
+
+ await stopRunningTransactions(station)
+
+ const txEventCalls = sentPayloads.filter(p => p.command === 'TransactionEvent')
+ assert.strictEqual(txEventCalls.length, 2)
+ const txIds = txEventCalls.map(p => p.transactionId)
+ assert.ok(txIds.includes('tx-001'))
+ assert.ok(txIds.includes('tx-002'))
+ })
+
+ await it('should include pending transactions in OCPP 2.0 stopRunningTransactions', async () => {
+ const sentPayloads: { command: string; transactionId?: string }[] = []
+ const { requestHandler, station } = createStationWithRequestHandler({
+ connectorsCount: 2,
+ evseConfiguration: { evsesCount: 2 },
+ ocppVersion: OCPPVersion.VERSION_20,
+ })
+ requestHandler.mock.mockImplementation(async (...args: unknown[]) => {
+ const payload = args[2] as Record<string, unknown>
+ sentPayloads.push({
+ command: args[1] as string,
+ transactionId: payload.transactionId as string | undefined,
+ })
+ return Promise.resolve({ idTokenInfo: { status: 'Accepted' } })
+ })
+ setupTransaction(station, 1, 'tx-started')
+ setupPendingTransaction(station, 2, 'tx-pending')
+
+ await stopRunningTransactions(station)
+
+ const txEventCalls = sentPayloads.filter(p => p.command === 'TransactionEvent')
+ assert.strictEqual(txEventCalls.length, 2)
+ })
+
+ await it('should handle errors gracefully when OCPP 2.0 transaction stop fails', async () => {
+ const { requestHandler, station } = createStationWithRequestHandler({
+ connectorsCount: 2,
+ evseConfiguration: { evsesCount: 2 },
+ ocppVersion: OCPPVersion.VERSION_20,
+ })
+ requestHandler.mock.mockImplementation(async () =>
+ Promise.reject(new Error('Simulated network error'))
+ )
+ setupTransaction(station, 1, 'tx-fail')
+
+ await assert.doesNotReject(() => stopRunningTransactions(station))
+ })
+ })
+
+ await describe('startTransactionOnConnector', async () => {
+ await it('should send StartTransaction for OCPP 1.6 stations and return accepted: true', async () => {
+ const { requestHandler, station } = createStationWithRequestHandler()
+ requestHandler.mock.mockImplementation(async (..._args: unknown[]) =>
+ Promise.resolve({ idTagInfo: { status: 'Accepted' }, transactionId: 1 })
+ )
+
+ const result = await startTransactionOnConnector(station, 1, 'TAG001')
+
+ assert.strictEqual(result.accepted, true)
+ assert.ok(requestHandler.mock.calls.length >= 1)
+ assert.strictEqual(requestHandler.mock.calls[0].arguments[1] as string, 'StartTransaction')
+ })
+
+ await it('should send TransactionEvent(Started) for OCPP 2.0 stations and return accepted: true', async () => {
+ const { requestHandler, station } = createStationWithRequestHandler({
+ evseConfiguration: { evsesCount: 1 },
+ ocppVersion: OCPPVersion.VERSION_20,
+ })
+ requestHandler.mock.mockImplementation(async (..._args: unknown[]) =>
+ Promise.resolve({ idTokenInfo: { status: 'Accepted' } })
+ )
+
+ const result = await startTransactionOnConnector(station, 1, 'TAG002')
+
+ assert.strictEqual(result.accepted, true)
+ assert.ok(requestHandler.mock.calls.length >= 1)
+ assert.strictEqual(requestHandler.mock.calls[0].arguments[1] as string, 'TransactionEvent')
+ })
+
+ await it('should generate transactionId for OCPP 2.0 when not pre-populated', async () => {
+ const { requestHandler, station } = createStationWithRequestHandler({
+ evseConfiguration: { evsesCount: 1 },
+ ocppVersion: OCPPVersion.VERSION_20,
+ })
+ requestHandler.mock.mockImplementation(async (..._args: unknown[]) =>
+ Promise.resolve({ idTokenInfo: { status: 'Accepted' } })
+ )
+ const connector = station.getConnectorStatus(1)
+ assert.notStrictEqual(connector, undefined)
+ assert(connector != null)
+ delete connector.transactionId
+
+ await startTransactionOnConnector(station, 1)
+
+ assert.notStrictEqual(connector.transactionId, undefined)
+ assert.strictEqual(typeof connector.transactionId, 'string')
+ })
+ })
+
+ await describe('flushQueuedTransactionMessages', async () => {
+ await it('should be a no-op for OCPP 1.6 stations', async () => {
+ const { station } = createStationWithRequestHandler()
+
+ await assert.doesNotReject(() => flushQueuedTransactionMessages(station))
+ })
+
+ await it('should flush queued events for OCPP 2.0 stations', async () => {
+ const { requestHandler, station } = createStationWithRequestHandler({
+ evseConfiguration: { evsesCount: 1 },
+ ocppVersion: OCPPVersion.VERSION_20,
+ })
+ requestHandler.mock.mockImplementation(async (..._args: unknown[]) => Promise.resolve({}))
+ const connector = station.getConnectorStatus(1)
+ assert.notStrictEqual(connector, undefined)
+ assert(connector != null)
+ connector.transactionEventQueue = [
+ {
+ request: {
+ eventType: 'Updated',
+ offline: true,
+ seqNo: 1,
+ timestamp: new Date().toISOString(),
+ transactionInfo: { transactionId: '550e8400-e29b-41d4-a716-446655440000' },
+ triggerReason: 'MeterValuePeriodic',
+ } as unknown as OCPP20TransactionEventRequest,
+ seqNo: 1,
+ timestamp: new Date(),
+ },
+ ]
+
+ await flushQueuedTransactionMessages(station)
+
+ assert.strictEqual(connector.transactionEventQueue.length, 0)
+ })
+ })
+
+ await describe('mapStopReasonToOCPP20', async () => {
+ await it('should map Other to Other/AbnormalCondition', () => {
+ const result = mapStopReasonToOCPP20('Other' as StopTransactionReason)
+
+ assert.strictEqual(result.stoppedReason, 'Other')
+ assert.strictEqual(result.triggerReason, 'AbnormalCondition')
+ })
+
+ await it('should map undefined to Local/StopAuthorized', () => {
+ const result = mapStopReasonToOCPP20(undefined)
+
+ assert.strictEqual(result.stoppedReason, 'Local')
+ assert.strictEqual(result.triggerReason, 'StopAuthorized')
+ })
+
+ await it('should map Remote to Remote/RemoteStop', () => {
+ const result = mapStopReasonToOCPP20('Remote' as StopTransactionReason)
+
+ assert.strictEqual(result.stoppedReason, 'Remote')
+ assert.strictEqual(result.triggerReason, 'RemoteStop')
+ })
+ })
+})