**src/assets/station-templates/\<name\>.json**:
-| Key | Value(s) | Default Value | Value type | Description |
-| ---------------------------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| supervisionUrls | | [] | string \| string[] | string or strings array containing connection URIs to OCPP-J servers |
-| supervisionUser | | undefined | string | basic HTTP authentication user to OCPP-J server |
-| supervisionPassword | | undefined | string | basic HTTP authentication password to OCPP-J server |
-| supervisionUrlOcppConfiguration | true/false | false | boolean | enable supervision URL configuration via a vendor OCPP parameter key |
-| supervisionUrlOcppKey | | 'ConnectionUrl' | string | the vendor string that will be used as a vendor OCPP parameter key to set the supervision URL |
-| autoStart | true/false | true | boolean | enable automatic start of added charging station from template |
-| ocppVersion | 1.6/2.0/2.0.1 | 1.6 | string | OCPP version |
-| ocppProtocol | json | json | string | OCPP protocol |
-| ocppStrictCompliance | true/false | true | boolean | enable strict adherence to the OCPP version and protocol specifications with OCPP commands PDU validation against [OCA](https://www.openchargealliance.org/) JSON schemas |
-| ocppPersistentConfiguration | true/false | true | boolean | enable persistent OCPP parameters storage by charging stations 'hashId'. The persistency is ensured by the charging stations configuration files in [dist/assets/configurations](./dist/assets/configurations) |
-| stationInfoPersistentConfiguration | true/false | true | boolean | enable persistent station information and specifications storage by charging stations 'hashId'. The persistency is ensured by the charging stations configuration files in [dist/assets/configurations](./dist/assets/configurations) |
-| automaticTransactionGeneratorPersistentConfiguration | true/false | true | boolean | enable persistent automatic transaction generator configuration storage by charging stations 'hashId'. The persistency is ensured by the charging stations configuration files in [dist/assets/configurations](./dist/assets/configurations) |
-| wsOptions | | {} | ClientOptions & ClientRequestArgs | [ws](https://github.com/websockets/ws) and node.js [http](https://nodejs.org/api/http.html) clients options intersection |
-| idTagsFile | | undefined | string | RFID tags list file relative to [src/assets](./src/assets) path |
-| iccid | | undefined | string | SIM card ICCID |
-| imsi | | undefined | string | SIM card IMSI |
-| baseName | | undefined | string | base name to build charging stations id |
-| nameSuffix | | undefined | string | name suffix to build charging stations id |
-| fixedName | true/false | false | boolean | use the 'baseName' as the charging stations unique name |
-| chargePointModel | | undefined | string | charging stations model |
-| chargePointVendor | | undefined | string | charging stations vendor |
-| chargePointSerialNumberPrefix | | undefined | string | charge point serial number prefix |
-| chargeBoxSerialNumberPrefix | | undefined | string | charge box serial number prefix (deprecated since OCPP 1.6) |
-| meterSerialNumberPrefix | | undefined | string | meter serial number prefix |
-| meterType | | undefined | string | meter type |
-| firmwareVersionPattern | | Semantic versioning regular expression: https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string | string | charging stations firmware version pattern |
-| firmwareVersion | | undefined | string | charging stations firmware version |
-| power | | | float \| float[] | charging stations maximum power value(s) |
-| powerSharedByConnectors | true/false | false | boolean | charging stations power shared by its connectors |
-| powerUnit | W/kW | W | string | charging stations power unit |
-| currentOutType | AC/DC | AC | string | charging stations current out type |
-| voltageOut | | AC:230/DC:400 | integer | charging stations voltage out |
-| numberOfPhases | 0/1/3 | AC:3/DC:0 | integer | charging stations number of phase(s) |
-| numberOfConnectors | | | integer \| integer[] | charging stations number of connector(s) |
-| useConnectorId0 | true/false | true | boolean | use connector id 0 definition from the charging station configuration template |
-| randomConnectors | true/false | false | boolean | randomize runtime connector id affectation from the connector id definition in charging station configuration template |
-| resetTime | | 60 | integer | seconds to wait before the charging stations come back at reset |
-| autoRegister | true/false | false | boolean | set charging stations as registered at boot notification for testing purpose |
-| autoReconnectMaxRetries | | -1 (unlimited) | integer | connection retries to the OCPP-J server |
-| reconnectExponentialDelay | true/false | false | boolean | connection delay retry to the OCPP-J server |
-| registrationMaxRetries | | -1 (unlimited) | integer | charging stations boot notification retries |
-| amperageLimitationOcppKey | | undefined | string | charging stations OCPP parameter key used to set the amperage limit, per phase for each connector on AC and global for DC |
-| amperageLimitationUnit | A/cA/dA/mA | A | string | charging stations amperage limit unit |
-| enableStatistics | true/false | false | boolean | enable charging stations statistics |
-| remoteAuthorization | true/false | true | boolean | enable RFID tags remote authorization |
-| beginEndMeterValues | true/false | false | boolean | enable Transaction.{Begin,End} MeterValues |
-| outOfOrderEndMeterValues | true/false | false | boolean | send Transaction.End MeterValues out of order. Need to relax OCPP specifications strict compliance ('ocppStrictCompliance' parameter) |
-| meteringPerTransaction | true/false | true | boolean | enable metering history on a per transaction basis |
-| transactionDataMeterValues | true/false | false | boolean | enable transaction data MeterValues at stop transaction |
-| stopTransactionsOnStopped | true/false | true | boolean | enable stop transactions on charging station stop |
-| mainVoltageMeterValues | true/false | true | boolean | include charging stations main voltage MeterValues on three phased charging stations |
-| phaseLineToLineVoltageMeterValues | true/false | false | boolean | include charging stations line to line voltage MeterValues on three phased charging stations |
-| customValueLimitationMeterValues | true/false | true | boolean | enable limitation on custom fluctuated value in MeterValues |
-| firmwareUpgrade | | {<br />"versionUpgrade": {<br />"step": 1<br />},<br />"reset": true<br />} | {<br />versionUpgrade?: {<br />patternGroup?: number;<br />step?: number;<br />};<br />reset?: boolean;<br />failureStatus?: 'DownloadFailed' \| 'InstallationFailed';<br />} | Configuration section for simulating firmware upgrade support. |
-| commandsSupport | | {<br />"incomingCommands": {},<br />"outgoingCommands": {}<br />} | {<br /> incomingCommands: Record<IncomingRequestCommand, boolean>;<br />outgoingCommands?: Record<RequestCommand, boolean>;<br />} | Configuration section for OCPP commands support. Empty section or subsections means all implemented OCPP commands are supported |
-| messageTriggerSupport | | {} | Record<MessageTrigger, boolean> | Configuration section for OCPP commands trigger support. Empty section means all implemented OCPP trigger commands are supported |
-| Configuration | | | ChargingStationOcppConfiguration | charging stations OCPP parameters configuration section |
-| AutomaticTransactionGenerator | | | AutomaticTransactionGeneratorConfiguration | charging stations ATG configuration section |
-| Connectors | | | Record<string, ConnectorStatus> | charging stations connectors configuration section |
-| Evses | | | Record<string, EvseTemplate> | charging stations EVSEs configuration section |
+| Key | Value(s) | Default Value | Value type | Description |
+| ---------------------------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| supervisionUrls | | [] | string \| string[] | string or strings array containing connection URIs to OCPP-J servers |
+| supervisionUser | | undefined | string | basic HTTP authentication user to OCPP-J server |
+| supervisionPassword | | undefined | string | basic HTTP authentication password to OCPP-J server |
+| supervisionUrlOcppConfiguration | true/false | false | boolean | enable supervision URL configuration via a vendor OCPP parameter key |
+| supervisionUrlOcppKey | | 'ConnectionUrl' | string | the vendor string that will be used as a vendor OCPP parameter key to set the supervision URL |
+| autoStart | true/false | true | boolean | enable automatic start of added charging station from template |
+| ocppVersion | 1.6/2.0/2.0.1 | 1.6 | string | OCPP version |
+| ocppProtocol | json | json | string | OCPP protocol |
+| ocppStrictCompliance | true/false | true | boolean | enable strict adherence to the OCPP version and protocol specifications with OCPP commands PDU validation against [OCA](https://www.openchargealliance.org/) JSON schemas |
+| ocppPersistentConfiguration | true/false | true | boolean | enable persistent OCPP parameters storage by charging stations 'hashId'. The persistency is ensured by the charging stations configuration files in [dist/assets/configurations](./dist/assets/configurations) |
+| stationInfoPersistentConfiguration | true/false | true | boolean | enable persistent station information and specifications storage by charging stations 'hashId'. The persistency is ensured by the charging stations configuration files in [dist/assets/configurations](./dist/assets/configurations) |
+| automaticTransactionGeneratorPersistentConfiguration | true/false | true | boolean | enable persistent automatic transaction generator configuration storage by charging stations 'hashId'. The persistency is ensured by the charging stations configuration files in [dist/assets/configurations](./dist/assets/configurations) |
+| wsOptions | | {} | ClientOptions & ClientRequestArgs | [ws](https://github.com/websockets/ws) and node.js [http](https://nodejs.org/api/http.html) clients options intersection |
+| idTagsFile | | undefined | string | RFID tags list file relative to [src/assets](./src/assets) path |
+| iccid | | undefined | string | SIM card ICCID |
+| imsi | | undefined | string | SIM card IMSI |
+| baseName | | undefined | string | base name to build charging stations id |
+| nameSuffix | | undefined | string | name suffix to build charging stations id |
+| fixedName | true/false | false | boolean | use the 'baseName' as the charging stations unique name |
+| chargePointModel | | undefined | string | charging stations model |
+| chargePointVendor | | undefined | string | charging stations vendor |
+| chargePointSerialNumberPrefix | | undefined | string | charge point serial number prefix |
+| chargeBoxSerialNumberPrefix | | undefined | string | charge box serial number prefix (deprecated since OCPP 1.6) |
+| meterSerialNumberPrefix | | undefined | string | meter serial number prefix |
+| meterType | | undefined | string | meter type |
+| firmwareVersionPattern | | Semantic versioning regular expression: https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string | string | charging stations firmware version pattern |
+| firmwareVersion | | undefined | string | charging stations firmware version |
+| power | | | float \| float[] | charging stations maximum power value(s) |
+| powerSharedByConnectors | true/false | false | boolean | charging stations power shared by its connectors |
+| powerUnit | W/kW | W | string | charging stations power unit |
+| currentOutType | AC/DC | AC | string | charging stations current out type |
+| voltageOut | | AC:230/DC:400 | integer | charging stations voltage out |
+| numberOfPhases | 0/1/3 | AC:3/DC:0 | integer | charging stations number of phase(s) |
+| numberOfConnectors | | | integer \| integer[] | charging stations number of connector(s) |
+| useConnectorId0 | true/false | true | boolean | use connector id 0 definition from the charging station configuration template |
+| randomConnectors | true/false | false | boolean | randomize runtime connector id affectation from the connector id definition in charging station configuration template |
+| resetTime | | 60 | integer | seconds to wait before the charging stations come back at reset |
+| autoRegister | true/false | false | boolean | set charging stations as registered at boot notification for testing purpose |
+| autoReconnectMaxRetries | | -1 (unlimited) | integer | connection retries to the OCPP-J server |
+| reconnectExponentialDelay | true/false | false | boolean | connection delay retry to the OCPP-J server |
+| registrationMaxRetries | | -1 (unlimited) | integer | charging stations boot notification retries |
+| amperageLimitationOcppKey | | undefined | string | charging stations OCPP parameter key used to set the amperage limit, per phase for each connector on AC and global for DC |
+| amperageLimitationUnit | A/cA/dA/mA | A | string | charging stations amperage limit unit |
+| enableStatistics | true/false | false | boolean | enable charging stations statistics |
+| remoteAuthorization | true/false | true | boolean | enable RFID tags remote authorization |
+| beginEndMeterValues | true/false | false | boolean | enable Transaction.{Begin,End} MeterValues |
+| outOfOrderEndMeterValues | true/false | false | boolean | send Transaction.End MeterValues out of order. Need to relax OCPP specifications strict compliance ('ocppStrictCompliance' parameter) |
+| meteringPerTransaction | true/false | true | boolean | enable metering history on a per transaction basis |
+| transactionDataMeterValues | true/false | false | boolean | enable transaction data MeterValues at stop transaction |
+| stopTransactionsOnStopped | true/false | true | boolean | enable stop transactions on charging station stop |
+| postTransactionDelay | ≥ 0 | 0 | integer | seconds to wait after transaction stop before transitioning connector to Available. Simulates cable-unplug delay. In OCPP 1.6 the connector stays in Finishing state; in OCPP 2.0.x it stays Occupied. 0 = immediate Available (default behavior) |
+| mainVoltageMeterValues | true/false | true | boolean | include charging stations main voltage MeterValues on three phased charging stations |
+| phaseLineToLineVoltageMeterValues | true/false | false | boolean | include charging stations line to line voltage MeterValues on three phased charging stations |
+| customValueLimitationMeterValues | true/false | true | boolean | enable limitation on custom fluctuated value in MeterValues |
+| firmwareUpgrade | | {<br />"versionUpgrade": {<br />"step": 1<br />},<br />"reset": true<br />} | {<br />versionUpgrade?: {<br />patternGroup?: number;<br />step?: number;<br />};<br />reset?: boolean;<br />failureStatus?: 'DownloadFailed' \| 'InstallationFailed';<br />} | Configuration section for simulating firmware upgrade support. |
+| commandsSupport | | {<br />"incomingCommands": {},<br />"outgoingCommands": {}<br />} | {<br /> incomingCommands: Record<IncomingRequestCommand, boolean>;<br />outgoingCommands?: Record<RequestCommand, boolean>;<br />} | Configuration section for OCPP commands support. Empty section or subsections means all implemented OCPP commands are supported |
+| messageTriggerSupport | | {} | Record<MessageTrigger, boolean> | Configuration section for OCPP commands trigger support. Empty section means all implemented OCPP trigger commands are supported |
+| Configuration | | | ChargingStationOcppConfiguration | charging stations OCPP parameters configuration section |
+| AutomaticTransactionGenerator | | | AutomaticTransactionGeneratorConfiguration | charging stations ATG configuration section |
+| Connectors | | | Record<string, ConnectorStatus> | charging stations connectors configuration section |
+| Evses | | | Record<string, EvseTemplate> | charging stations EVSEs configuration section |
#### Configuration section syntax example
): void => {
for (const [connectorId, connectorStatus] of connectors) {
if (connectorId > 0 && connectorStatus.transactionStarted === true) {
- logger.warn(
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- `${logPrefix} ${moduleName}.initializeConnectorsMapStatus: Connector id ${connectorId.toString()} at initialization has a transaction started with id ${connectorStatus.transactionId?.toString()}`
- )
+ if (
+ connectorStatus.transactionId == null ||
+ connectorStatus.status === ConnectorStatusEnum.Finishing
+ ) {
+ resetConnectorStatus(connectorStatus)
+ connectorStatus.locked = false
+ logger.warn(
+ `${logPrefix} ${moduleName}.initializeConnectorsMapStatus: Connector id ${connectorId.toString()} at initialization has stale transaction state, resetting`
+ )
+ } else {
+ logger.warn(
+ `${logPrefix} ${moduleName}.initializeConnectorsMapStatus: Connector id ${connectorId.toString()} at initialization has a transaction started with id ${connectorStatus.transactionId.toString()}`
+ )
+ }
}
if (connectorId === 0) {
connectorStatus.availability = AvailabilityType.Operative
idTag
)
}
+ if (
+ chargingStation.getConnectorStatus(transactionConnectorId)?.status ===
+ OCPP16ChargePointStatus.Finishing
+ ) {
+ return this.notifyRemoteStartTransactionRejected(
+ chargingStation,
+ transactionConnectorId,
+ idTag
+ )
+ }
if (
!chargingStation.isChargingStationAvailable() ||
!chargingStation.isConnectorAvailable(transactionConnectorId)
} from '../../../charging-station/index.js'
import {
ChargingStationEvents,
+ type ConnectorStatus,
type JsonType,
OCPP16AuthorizationStatus,
type OCPP16AuthorizeRequest,
ReservationTerminationReason,
type ResponseHandler,
} from '../../../types/index.js'
-import { Constants, convertToInt, logger, truncateId } from '../../../utils/index.js'
+import { Constants, convertToInt, logger, sleep, truncateId } from '../../../utils/index.js'
import {
restoreConnectorStatus,
sendAndSetConnectorStatus,
+ sendPostTransactionStatus,
} from '../OCPPConnectorStatusOperations.js'
import { OCPPResponseService } from '../OCPPResponseService.js'
import { createPayloadValidatorMap, isRequestCommandSupported } from '../OCPPServiceUtils.js'
const moduleName = 'OCPP16ResponseService'
+const decrementPowerDivider = (chargingStation: ChargingStation): void => {
+ if (chargingStation.stationInfo?.powerSharedByConnectors === true) {
+ if (chargingStation.powerDivider != null && chargingStation.powerDivider > 0) {
+ --chargingStation.powerDivider
+ } else {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.handleResponseStopTransaction: powerDivider is ${
+ chargingStation.powerDivider?.toString() ?? 'undefined'
+ }, cannot decrement`
+ )
+ }
+ }
+}
+
+const finalizeTransactionConnectorStatus = (
+ connectorStatus: ConnectorStatus | undefined,
+ requestPayload: OCPP16StopTransactionRequest
+): string | undefined => {
+ const transactionIdTag = requestPayload.idTag ?? connectorStatus?.transactionIdTag
+ resetConnectorStatus(connectorStatus)
+ if (connectorStatus != null) {
+ connectorStatus.locked = false
+ }
+ return transactionIdTag
+}
+
/**
* OCPP 1.6 Response Service - handles and processes all outgoing request responses
* from the Charging Station (CP) to the Central System (CS) using OCPP 1.6 protocol.
],
transactionId: requestPayload.transactionId,
}))
- if (
- !chargingStation.isChargingStationAvailable() ||
- !chargingStation.isConnectorAvailable(transactionConnectorId)
- ) {
- await sendAndSetConnectorStatus(chargingStation, {
- connectorId: transactionConnectorId,
- status: OCPP16ChargePointStatus.Unavailable,
- } as OCPP16StatusNotificationRequest)
- } else {
- await sendAndSetConnectorStatus(chargingStation, {
- connectorId: transactionConnectorId,
- status: OCPP16ChargePointStatus.Available,
- } as OCPP16StatusNotificationRequest)
- }
- if (chargingStation.stationInfo?.powerSharedByConnectors === true) {
- if (chargingStation.powerDivider != null && chargingStation.powerDivider > 0) {
- --chargingStation.powerDivider
- } else {
- logger.error(
- `${chargingStation.logPrefix()} ${moduleName}.handleResponseStopTransaction: powerDivider is ${
- chargingStation.powerDivider?.toString() ?? 'undefined'
- }, cannot decrement`
- )
+ const postTransactionDelay = chargingStation.stationInfo?.postTransactionDelay ?? 0
+ let transactionIdTag: string | undefined
+ if (postTransactionDelay > 0) {
+ decrementPowerDivider(chargingStation)
+ // Send Finishing status if not already set (idempotency guard)
+ const transactionConnectorStatus = chargingStation.getConnectorStatus(transactionConnectorId)
+ if (transactionConnectorStatus?.status !== OCPP16ChargePointStatus.Finishing) {
+ await sendAndSetConnectorStatus(chargingStation, {
+ connectorId: transactionConnectorId,
+ status: OCPP16ChargePointStatus.Finishing,
+ } as OCPP16StatusNotificationRequest)
}
+ OCPP16ServiceUtils.stopUpdatedMeterValues(chargingStation, transactionConnectorId)
+ if (transactionConnectorStatus != null) {
+ delete transactionConnectorStatus.transactionId
+ }
+ await sleep(secondsToMilliseconds(postTransactionDelay))
+ if (!chargingStation.started) {
+ return
+ }
+ transactionIdTag = finalizeTransactionConnectorStatus(
+ transactionConnectorStatus,
+ requestPayload
+ )
+ await sendPostTransactionStatus(chargingStation, transactionConnectorId)
+ } else {
+ await sendPostTransactionStatus(chargingStation, transactionConnectorId)
+ decrementPowerDivider(chargingStation)
+ const transactionConnectorStatus = chargingStation.getConnectorStatus(transactionConnectorId)
+ transactionIdTag = finalizeTransactionConnectorStatus(
+ transactionConnectorStatus,
+ requestPayload
+ )
+ OCPP16ServiceUtils.stopUpdatedMeterValues(chargingStation, transactionConnectorId)
}
- const transactionConnectorStatus = chargingStation.getConnectorStatus(transactionConnectorId)
- const transactionIdTag = requestPayload.idTag ?? transactionConnectorStatus?.transactionIdTag
- resetConnectorStatus(transactionConnectorStatus)
- if (
- transactionConnectorStatus != null &&
- (payload.idTagInfo == null || payload.idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED)
- ) {
- transactionConnectorStatus.locked = false
- }
- OCPP16ServiceUtils.stopUpdatedMeterValues(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
if (
connectorStatus.transactionStarted === true ||
- connectorStatus.transactionPending === true
+ connectorStatus.transactionPending === true ||
+ connectorStatus.locked === true
) {
logger.warn(
`${chargingStation.logPrefix()} ${moduleName}.handleRequestStartTransaction: Connector ${connectorId.toString()} already has an active or pending transaction`
import { OCPPError } from '../../../exception/index.js'
import {
type ConnectorStatus,
- ConnectorStatusEnum,
+ type ConnectorStatusEnum,
ErrorType,
type MeterValue,
OCPP20AuthorizationStatusEnumType,
generateUUID,
isNotEmptyArray,
logger,
+ sleep,
validateIdentifierString,
} from '../../../utils/index.js'
import { buildConfigKey, getConfigurationKey } from '../../index.js'
mapOCPP20TokenType,
OCPPAuthServiceFactory,
} from '../auth/index.js'
-import { sendAndSetConnectorStatus } from '../OCPPConnectorStatusOperations.js'
+import { sendPostTransactionStatus } from '../OCPPConnectorStatusOperations.js'
import {
buildMeterValue,
createPayloadConfigs,
return
}
OCPP20ServiceUtils.stopUpdatedMeterValues(chargingStation, connectorId)
+ const postTransactionDelay = chargingStation.stationInfo?.postTransactionDelay ?? 0
+ if (postTransactionDelay > 0) {
+ delete connectorStatus.transactionId
+ await sleep(secondsToMilliseconds(postTransactionDelay))
+ if (!chargingStation.started) {
+ return
+ }
+ }
resetConnectorStatus(connectorStatus)
connectorStatus.locked = false
- const targetStatus =
- chargingStation.isChargingStationAvailable() &&
- chargingStation.isConnectorAvailable(connectorId)
- ? ConnectorStatusEnum.Available
- : ConnectorStatusEnum.Unavailable
- await sendAndSetConnectorStatus(chargingStation, {
- connectorId,
- connectorStatus: targetStatus,
- } as unknown as OCPP20StatusNotificationRequest)
+ await sendPostTransactionStatus(chargingStation, connectorId)
}
/**
})
}
+/**
+ * Sends Available or Unavailable connector status after a transaction ends.
+ * Re-evaluates station and connector availability to determine the target status.
+ * @param chargingStation - Target charging station
+ * @param connectorId - Connector ID to transition
+ */
+export const sendPostTransactionStatus = async (
+ chargingStation: ChargingStation,
+ connectorId: number
+): Promise<void> => {
+ const status =
+ chargingStation.isChargingStationAvailable() &&
+ chargingStation.isConnectorAvailable(connectorId)
+ ? ConnectorStatusEnum.Available
+ : ConnectorStatusEnum.Unavailable
+ await sendAndSetConnectorStatus(chargingStation, {
+ connectorId,
+ connectorStatus: status,
+ status,
+ } as unknown as StatusNotificationRequest)
+}
+
/**
* Restores a connector status to Reserved or Available based on its current state.
* @param chargingStation - Target charging station
import { OCPPError } from '../../exception/index.js'
import {
AuthorizationStatus,
+ ConnectorStatusEnum,
ErrorType,
OCPPVersion,
type StartTransactionResult,
switch (chargingStation.stationInfo?.ocppVersion) {
case OCPPVersion.VERSION_16: {
for (const { connectorId, connectorStatus } of chargingStation.iterateConnectors(true)) {
- if (connectorStatus.transactionStarted === true) {
+ if (
+ connectorStatus.transactionStarted === true &&
+ connectorStatus.status !== ConnectorStatusEnum.Finishing
+ ) {
await OCPP16ServiceUtils.stopTransactionOnConnector(chargingStation, connectorId, reason)
}
}
/** @deprecated Replaced by ocppStrictCompliance. */
payloadSchemaValidation?: boolean
phaseLineToLineVoltageMeterValues?: boolean
+ /** Seconds to wait after transaction stop before transitioning connector to Available. Simulates cable-unplug delay. In OCPP 1.6 the connector stays in Finishing state; in OCPP 2.0.x it stays Occupied. 0 = immediate Available (default behavior). */
+ postTransactionDelay?: number
power?: number | number[]
powerSharedByConnectors?: boolean
powerUnit?: PowerUnits
ocppVersion: OCPPVersion.VERSION_16,
outOfOrderEndMeterValues: false,
phaseLineToLineVoltageMeterValues: false,
+ postTransactionDelay: 0,
reconnectExponentialDelay: false,
registrationMaxRetries: -1,
remoteAuthorization: true,
import {
AvailabilityType,
GenericStatus,
+ OCPP16ChargePointStatus,
OCPP16IncomingRequestCommand,
OCPP16RequestCommand,
} from '../../../../src/types/index.js'
assert.strictEqual(response.status, GenericStatus.Rejected)
})
+ // --- Finishing state guard ---
+
+ await it('should reject remote start transaction when connector is in Finishing state', async () => {
+ // Arrange
+ const { station, testableService } = testContext
+ const connectorStatus = station.getConnectorStatus(1)
+ if (connectorStatus != null) {
+ connectorStatus.status = OCPP16ChargePointStatus.Finishing
+ }
+ const request: RemoteStartTransactionRequest = {
+ connectorId: 1,
+ idTag: TEST_ID_TAG,
+ }
+
+ // Act
+ const response = await testableService.handleRequestRemoteStartTransaction(station, request)
+
+ // Assert
+ assert.strictEqual(response.status, GenericStatus.Rejected)
+ })
+
// --- Event listeners ---
await describe('REMOTE_START_TRANSACTION event listener', async () => {
--- /dev/null
+/**
+ * @file Tests for OCPP16ResponseService postTransactionDelay
+ * @description Verifies the postTransactionDelay feature in OCPP 1.6 StopTransaction response
+ * handling: Finishing→Available transitions, zero-delay immediate transitions,
+ * availability re-evaluation, and shutdown-during-delay safety.
+ */
+
+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 { OCPP16ResponseService } from '../../../../src/charging-station/ocpp/1.6/OCPP16ResponseService.js'
+import type {
+ OCPP16StopTransactionRequest,
+ OCPP16StopTransactionResponse,
+} from '../../../../src/types/index.js'
+
+import { OCPP16ServiceUtils } from '../../../../src/charging-station/ocpp/1.6/OCPP16ServiceUtils.js'
+import {
+ AvailabilityType,
+ OCPP16AuthorizationStatus,
+ OCPP16ChargePointStatus,
+ OCPP16RequestCommand,
+} from '../../../../src/types/index.js'
+import {
+ flushMicrotasks,
+ setupConnectorWithTransaction,
+ standardCleanup,
+ withMockTimers,
+} from '../../../helpers/TestLifecycleHelpers.js'
+import { createOCPP16ResponseTestContext, setMockRequestHandler } from './OCPP16TestUtils.js'
+
+await describe('OCPP16ResponseService — PostTransactionDelay', async () => {
+ let station: ChargingStation
+ let responseService: OCPP16ResponseService
+ let requestCalls: unknown[][]
+
+ beforeEach(() => {
+ const ctx = createOCPP16ResponseTestContext({
+ stationInfo: { postTransactionDelay: 5 },
+ })
+ station = ctx.station
+ responseService = ctx.responseService
+ station.started = true
+
+ requestCalls = []
+ setMockRequestHandler(station, (...args: unknown[]) => {
+ requestCalls.push(args)
+ return Promise.resolve({})
+ })
+
+ mock.method(OCPP16ServiceUtils, 'startUpdatedMeterValues', () => {
+ /* noop */
+ })
+ mock.method(OCPP16ServiceUtils, 'stopUpdatedMeterValues', () => {
+ /* noop */
+ })
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ await it('should send Finishing then Available after configured delay', async t => {
+ // Arrange
+ setupConnectorWithTransaction(station, 1, { transactionId: 100 })
+ const requestPayload: OCPP16StopTransactionRequest = {
+ meterStop: 1000,
+ timestamp: new Date(),
+ transactionId: 100,
+ }
+ const responsePayload: OCPP16StopTransactionResponse = {
+ idTagInfo: { status: OCPP16AuthorizationStatus.ACCEPTED },
+ }
+
+ // Act
+ await withMockTimers(t, ['setTimeout'], async () => {
+ const promise = responseService.responseHandler(
+ station,
+ OCPP16RequestCommand.STOP_TRANSACTION,
+ responsePayload,
+ requestPayload
+ )
+ for (let i = 0; i < 10; i++) {
+ await flushMicrotasks()
+ }
+ t.mock.timers.tick(5000)
+ for (let i = 0; i < 10; i++) {
+ await flushMicrotasks()
+ }
+ await promise
+ })
+
+ // Assert
+ const statusCalls = requestCalls.filter(
+ call =>
+ call[1] === OCPP16RequestCommand.STATUS_NOTIFICATION &&
+ (call[2] as Record<string, unknown>).connectorId === 1
+ )
+ assert.ok(
+ statusCalls.length >= 2,
+ `Expected at least 2 status calls, got ${String(statusCalls.length)}`
+ )
+ assert.strictEqual(
+ (statusCalls[0][2] as Record<string, unknown>).status,
+ OCPP16ChargePointStatus.Finishing
+ )
+ assert.strictEqual(
+ (statusCalls[1][2] as Record<string, unknown>).status,
+ OCPP16ChargePointStatus.Available
+ )
+ })
+
+ await it('should send Available immediately when postTransactionDelay is 0', async () => {
+ // Arrange
+ assert.ok(station.stationInfo != null, 'stationInfo should be defined')
+ station.stationInfo.postTransactionDelay = 0
+ setupConnectorWithTransaction(station, 1, { transactionId: 200 })
+ const requestPayload: OCPP16StopTransactionRequest = {
+ meterStop: 2000,
+ timestamp: new Date(),
+ transactionId: 200,
+ }
+ const responsePayload: OCPP16StopTransactionResponse = {
+ idTagInfo: { status: OCPP16AuthorizationStatus.ACCEPTED },
+ }
+
+ // Act
+ await responseService.responseHandler(
+ station,
+ OCPP16RequestCommand.STOP_TRANSACTION,
+ responsePayload,
+ requestPayload
+ )
+
+ // Assert
+ const statusCalls = requestCalls.filter(
+ call =>
+ call[1] === OCPP16RequestCommand.STATUS_NOTIFICATION &&
+ (call[2] as Record<string, unknown>).connectorId === 1
+ )
+ const finishingCalls = statusCalls.filter(
+ call => (call[2] as Record<string, unknown>).status === OCPP16ChargePointStatus.Finishing
+ )
+ assert.strictEqual(
+ finishingCalls.length,
+ 0,
+ 'No Finishing status should be sent when delay is 0'
+ )
+ const availableCalls = statusCalls.filter(
+ call => (call[2] as Record<string, unknown>).status === OCPP16ChargePointStatus.Available
+ )
+ assert.ok(availableCalls.length >= 1, 'Should send Available status')
+ })
+
+ await it('should send Unavailable after delay when station becomes unavailable during finishing', async t => {
+ // Arrange
+ setupConnectorWithTransaction(station, 1, { transactionId: 300 })
+ const connector0 = station.getConnectorStatus(0)
+ if (connector0 != null) {
+ connector0.availability = AvailabilityType.Inoperative
+ }
+
+ const requestPayload: OCPP16StopTransactionRequest = {
+ meterStop: 3000,
+ timestamp: new Date(),
+ transactionId: 300,
+ }
+ const responsePayload: OCPP16StopTransactionResponse = {
+ idTagInfo: { status: OCPP16AuthorizationStatus.ACCEPTED },
+ }
+
+ // Act
+ await withMockTimers(t, ['setTimeout'], async () => {
+ const promise = responseService.responseHandler(
+ station,
+ OCPP16RequestCommand.STOP_TRANSACTION,
+ responsePayload,
+ requestPayload
+ )
+ for (let i = 0; i < 10; i++) {
+ await flushMicrotasks()
+ }
+ t.mock.timers.tick(5000)
+ for (let i = 0; i < 10; i++) {
+ await flushMicrotasks()
+ }
+ await promise
+ })
+
+ // Assert
+ const statusCalls = requestCalls.filter(
+ call =>
+ call[1] === OCPP16RequestCommand.STATUS_NOTIFICATION &&
+ (call[2] as Record<string, unknown>).connectorId === 1
+ )
+ assert.ok(
+ statusCalls.length >= 2,
+ `Expected at least 2 status calls, got ${String(statusCalls.length)}`
+ )
+ assert.strictEqual(
+ (statusCalls[0][2] as Record<string, unknown>).status,
+ OCPP16ChargePointStatus.Finishing
+ )
+ assert.strictEqual(
+ (statusCalls[1][2] as Record<string, unknown>).status,
+ OCPP16ChargePointStatus.Unavailable
+ )
+ })
+
+ await it('should skip cleanup when station stops during delay', async t => {
+ // Arrange
+ setupConnectorWithTransaction(station, 1, { transactionId: 400 })
+ const requestPayload: OCPP16StopTransactionRequest = {
+ meterStop: 4000,
+ timestamp: new Date(),
+ transactionId: 400,
+ }
+ const responsePayload: OCPP16StopTransactionResponse = {
+ idTagInfo: { status: OCPP16AuthorizationStatus.ACCEPTED },
+ }
+
+ // Act
+ await withMockTimers(t, ['setTimeout'], async () => {
+ const promise = responseService.responseHandler(
+ station,
+ OCPP16RequestCommand.STOP_TRANSACTION,
+ responsePayload,
+ requestPayload
+ )
+ for (let i = 0; i < 10; i++) {
+ await flushMicrotasks()
+ }
+ station.started = false
+ t.mock.timers.tick(5000)
+ for (let i = 0; i < 10; i++) {
+ await flushMicrotasks()
+ }
+ await promise
+ })
+
+ // Assert
+ const statusCalls = requestCalls.filter(
+ call =>
+ call[1] === OCPP16RequestCommand.STATUS_NOTIFICATION &&
+ (call[2] as Record<string, unknown>).connectorId === 1
+ )
+ assert.strictEqual(statusCalls.length, 1, 'Only Finishing status should be sent')
+ assert.strictEqual(
+ (statusCalls[0][2] as Record<string, unknown>).status,
+ OCPP16ChargePointStatus.Finishing
+ )
+ const connectorStatus = station.getConnectorStatus(1)
+ if (connectorStatus == null) {
+ assert.fail('Expected connector 1 to exist')
+ }
+ assert.strictEqual(connectorStatus.transactionStarted, true)
+ assert.strictEqual(connectorStatus.transactionId, undefined)
+ })
+})
--- /dev/null
+/**
+ * @file Tests for OCPP20ServiceUtils postTransactionDelay
+ * @description Verifies the postTransactionDelay feature in OCPP 2.0.x cleanupEndedTransaction:
+ * delayed Available transitions, zero-delay immediate transitions,
+ * and shutdown-during-delay safety.
+ */
+
+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 { ConnectorStatus } from '../../../../src/types/index.js'
+
+import { OCPP20ServiceUtils } from '../../../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js'
+import { OCPPVersion } from '../../../../src/types/index.js'
+import {
+ flushMicrotasks,
+ standardCleanup,
+ withMockTimers,
+} from '../../../helpers/TestLifecycleHelpers.js'
+import { createMockChargingStation } from '../../helpers/StationHelpers.js'
+
+await describe('OCPP20ServiceUtils — PostTransactionDelay', async () => {
+ let station: ChargingStation
+ let connectorStatus: ConnectorStatus
+ let requestHandlerMock: ReturnType<typeof mock.fn>
+
+ beforeEach(() => {
+ const requestHandler = mock.fn(async () => Promise.resolve({}))
+ requestHandlerMock = requestHandler
+ const result = createMockChargingStation({
+ connectorsCount: 1,
+ ocppRequestService: {
+ requestHandler: requestHandler as (...args: unknown[]) => Promise<unknown>,
+ },
+ ocppVersion: OCPPVersion.VERSION_20,
+ started: true,
+ stationInfo: {
+ ocppVersion: OCPPVersion.VERSION_20,
+ postTransactionDelay: 3,
+ },
+ })
+ station = result.station
+ const cs = station.getConnectorStatus(1)
+ if (cs == null) {
+ throw new Error('Expected connector 1 to exist')
+ }
+ connectorStatus = cs
+ connectorStatus.transactionStarted = true
+ connectorStatus.transactionId = 'tx-1'
+ connectorStatus.locked = true
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ await it('should delay Available transition after transaction end', async t => {
+ // Act
+ await withMockTimers(t, ['setTimeout'], async () => {
+ const promise = OCPP20ServiceUtils.cleanupEndedTransaction(station, 1, connectorStatus)
+ for (let i = 0; i < 10; i++) {
+ await flushMicrotasks()
+ }
+ t.mock.timers.tick(3000)
+ for (let i = 0; i < 10; i++) {
+ await flushMicrotasks()
+ }
+ await promise
+ })
+
+ // Assert
+ assert.strictEqual(connectorStatus.transactionStarted, false)
+ assert.strictEqual(connectorStatus.transactionId, undefined)
+ assert.strictEqual(connectorStatus.locked, false)
+ assert.ok(requestHandlerMock.mock.calls.length >= 1, 'Should send StatusNotification')
+ })
+
+ await it('should send Available immediately when postTransactionDelay is 0', async () => {
+ // Arrange
+ assert.ok(station.stationInfo != null, 'stationInfo should be defined')
+ station.stationInfo.postTransactionDelay = 0
+
+ // Act
+ await OCPP20ServiceUtils.cleanupEndedTransaction(station, 1, connectorStatus)
+
+ // Assert
+ assert.strictEqual(connectorStatus.transactionStarted, false)
+ assert.strictEqual(connectorStatus.transactionId, undefined)
+ assert.strictEqual(connectorStatus.locked, false)
+ assert.ok(requestHandlerMock.mock.calls.length >= 1, 'Should send StatusNotification')
+ })
+
+ await it('should skip cleanup when station stops during delay', async t => {
+ // Act
+ await withMockTimers(t, ['setTimeout'], async () => {
+ const promise = OCPP20ServiceUtils.cleanupEndedTransaction(station, 1, connectorStatus)
+ for (let i = 0; i < 10; i++) {
+ await flushMicrotasks()
+ }
+ station.started = false
+ t.mock.timers.tick(3000)
+ for (let i = 0; i < 10; i++) {
+ await flushMicrotasks()
+ }
+ await promise
+ })
+
+ // Assert — transactionStarted stays true (blocks ATG), transactionId cleared (blocks stopAll)
+ assert.strictEqual(connectorStatus.transactionStarted, true)
+ assert.strictEqual(connectorStatus.transactionId, undefined)
+ assert.strictEqual(connectorStatus.locked, true)
+ assert.strictEqual(
+ requestHandlerMock.mock.calls.length,
+ 0,
+ 'No StatusNotification should be sent'
+ )
+ })
+})