* feat(ocpp2): add type definitions and validator configs for OCPP 2.0.1 Core certification commands
* feat(ocpp2): implement DataTransfer reject handler
* feat(ocpp2): implement SetNetworkProfile reject handler
* feat(ocpp2): implement GetTransactionStatus handler
* feat(ocpp2): implement CustomerInformation clear handler
* feat(ocpp2): implement SecurityEventNotification outgoing command
* fix(ocpp2): handle TransactionEvent response idTokenInfo status
* feat(ocpp2): implement ChangeAvailability handler
* feat(ocpp2): implement standalone MeterValues outgoing command
* feat(ocpp2): implement UpdateFirmware and FirmwareStatusNotification
Add UpdateFirmware (CSMS→CS) incoming request handler with simulated
firmware update lifecycle state machine (Downloading → Downloaded →
SignatureVerified → Installing → Installed) and FirmwareStatusNotification
(CS→CSMS) outgoing command.
- Handle UPDATE_FIRMWARE incoming request, return Accepted status
- Simulate firmware update lifecycle via chained setTimeout calls
- Send FirmwareStatusNotification at each state transition
- Check firmware.signature presence for SignatureVerified state
- Add testable interfaces for both handler and request service
- Add 8 tests (5 for UpdateFirmware, 3 for FirmwareStatusNotification)
* feat(ocpp2): implement GetLog and LogStatusNotification
- Add GetLog incoming request handler with simulated upload lifecycle
(Uploading → Uploaded via chained setTimeout)
- Add LogStatusNotification outgoing command in RequestService
- Register handleResponseLogStatusNotification in ResponseService
- Update testable interfaces with new handler and request method
- Add 4 GetLog tests (DiagnosticsLog, SecurityLog, requestId, retries)
- Add 3 LogStatusNotification tests (Uploading, requestId, empty response)
- All quality gates pass: lint, typecheck, build, 1737 tests
* feat(ocpp2): expand TriggerMessage handler with new trigger types
* docs: update README with OCPP 2.0.1 Core certification commands
* style(ocpp2): fix space-before-function-paren in TriggerMessage handler and test
* fix(ocpp2): add missing case branches in buildRequestPayload for new commands
buildRequestPayload throws NOT_SUPPORTED for FirmwareStatusNotification,
LogStatusNotification, MeterValues, NotifyCustomerInformation, and
SecurityEventNotification since they have no case branches. This causes
guaranteed runtime failures when TriggerMessage invokes requestHandler
for these commands.
Add pass-through case branches matching the existing pattern used by
other notification commands.
* fix(ocpp2): use zero-based seqNo in NotifyCustomerInformation per OCPP 2.0.1 spec
* fix(ocpp2): stop only specific transaction in handleResponseTransactionEvent
handleResponseTransactionEvent was stopping ALL active transactions when
any rejected idTokenInfo.status arrived. Per OCPP 2.0.1 spec (D01/D05),
only the specific transaction referenced by the TransactionEvent request
should be stopped.
Extract the transactionId from the request payload and use
getConnectorIdByTransactionId/getEvseIdByTransactionId to find and stop
only the affected transaction.
* fix(ocpp2): set idle EVSEs Inoperative immediately on CS-level ChangeAvailability per G03.FR.04
* fix(ocpp2): add firmware lifecycle delay, JSDoc, and consistent handler patterns
- Add delay between Downloaded and SignatureVerified in firmware update lifecycle per J01
- Add missing JSDoc for requestLogStatusNotification in testable interface
- Convert 4 arrow function handlers to regular methods for consistency with pre-existing handlers
* refactor(ocpp2): resolve SonarCloud quality gate findings
- Replace duplicated delay() functions with shared sleep() utility
- Extract handleEvseChangeAvailability and handleCsLevelInoperative to
reduce cognitive complexity of handleRequestChangeAvailability
- Extract hasAnyActiveTransaction to eliminate nested loops
- Fix negated conditions and nested template literals
* refactor(ocpp2): deduplicate validator configs via shared schema name maps
- Extract incomingRequestSchemaNames and outgoingRequestSchemaNames as
single source of truth for command-to-schema mappings
- Generate request/response configs from shared maps, eliminating 96
lines of structural duplication flagged by SonarCloud
- Fix remaining negated conditions in ternary expressions
* fix(test): deduplicate MeterValues call, add multi-EVSE isolation test
* fix(ocpp2): eliminate double status notification, document messagesInQueue
* test(ocpp2): align test files with TEST_STYLE_GUIDE conventions
* fix: comply with E14.FR.06 and harmonize statusInfo
### Version 2.0.x
-> **Note**: OCPP 2.0.x implementation is **partial** and under active development.
+> **Note**: OCPP 2.0.x Core profile mandatory commands are now implemented.
+
+#### A. Security
+
+- :white_check_mark: SecurityEventNotification
#### B. Provisioning
- :white_check_mark: GetBaseReport
- :white_check_mark: GetVariables
- :white_check_mark: NotifyReport
+- :white_check_mark: SetNetworkProfile
- :white_check_mark: SetVariables
#### C. Authorization
#### E. Transactions
+- :white_check_mark: GetTransactionStatus
- :white_check_mark: RequestStartTransaction
- :white_check_mark: RequestStopTransaction
- :white_check_mark: TransactionEvent
#### G. Availability
+- :white_check_mark: ChangeAvailability
- :white_check_mark: Heartbeat
+- :white_check_mark: MeterValues
- :white_check_mark: StatusNotification
+#### J. Diagnostics
+
+- :white_check_mark: GetLog
+- :white_check_mark: LogStatusNotification
+
#### L. FirmwareManagement
-- :x: UpdateFirmware
-- :x: FirmwareStatusNotification
+- :white_check_mark: FirmwareStatusNotification
+- :white_check_mark: UpdateFirmware
#### M. ISO 15118 CertificateManagement
> - **Mock CSR generation**: The `SignCertificate` command generates a mock Certificate Signing Request (CSR) for simulation purposes. In production, this should be replaced with actual cryptographic CSR generation.
> - **OCSP stub**: Online Certificate Status Protocol (OCSP) validation is stubbed and returns `Failed` status. Full OCSP integration requires external OCSP responder configuration.
+#### N. CustomerInformation
+
+- :white_check_mark: CustomerInformation
+
#### P. DataTransfer
-- :x: DataTransfer
+- :white_check_mark: DataTransfer
## OCPP-J standard parameters supported
'CALLERROR',
'CALLRESULTERROR',
'reservability',
+ // VPN protocol acronyms
+ 'PPTP',
],
},
},
import {
type ConnectorStatusTransition,
+ MessageTriggerEnumType,
OCPP20ConnectorStatusEnumType,
OCPP20TriggerReasonEnumType,
} from '../../../types/index.js'
*/
static readonly HANDLER_TIMEOUT_MS = 30_000
+ /**
+ * Set of MessageTriggerEnumType values that the charging station supports
+ * in the TriggerMessage handler. Used for validation and capability reporting.
+ */
+ static readonly SupportedTriggerMessages: ReadonlySet<MessageTriggerEnumType> = new Set([
+ MessageTriggerEnumType.BootNotification,
+ MessageTriggerEnumType.FirmwareStatusNotification,
+ MessageTriggerEnumType.Heartbeat,
+ MessageTriggerEnumType.LogStatusNotification,
+ MessageTriggerEnumType.MeterValues,
+ MessageTriggerEnumType.StatusNotification,
+ ])
+
static readonly TriggerReasonMapping: readonly TriggerReasonMap[] = Object.freeze([
// Priority 1: Remote Commands (highest priority)
{
import {
AttributeEnumType,
CertificateSigningUseEnumType,
+ ChangeAvailabilityStatusEnumType,
ConnectorEnumType,
ConnectorStatusEnum,
+ CustomerInformationStatusEnumType,
DataEnumType,
+ DataTransferStatusEnumType,
DeleteCertificateStatusEnumType,
ErrorType,
type EvseStatus,
FirmwareStatus,
+ FirmwareStatusEnumType,
GenericDeviceModelStatusEnumType,
GenericStatus,
GetCertificateIdUseEnumType,
InstallCertificateStatusEnumType,
InstallCertificateUseEnumType,
type JsonType,
+ LogStatusEnumType,
MessageTriggerEnumType,
type OCPP20BootNotificationRequest,
type OCPP20BootNotificationResponse,
type OCPP20CertificateSignedRequest,
type OCPP20CertificateSignedResponse,
+ type OCPP20ChangeAvailabilityRequest,
+ type OCPP20ChangeAvailabilityResponse,
type OCPP20ClearCacheResponse,
OCPP20ComponentName,
OCPP20ConnectorStatusEnumType,
+ type OCPP20CustomerInformationRequest,
+ type OCPP20CustomerInformationResponse,
+ type OCPP20DataTransferRequest,
+ type OCPP20DataTransferResponse,
type OCPP20DeleteCertificateRequest,
type OCPP20DeleteCertificateResponse,
OCPP20DeviceInfoVariableName,
+ type OCPP20FirmwareStatusNotificationRequest,
+ type OCPP20FirmwareStatusNotificationResponse,
type OCPP20GetBaseReportRequest,
type OCPP20GetBaseReportResponse,
type OCPP20GetInstalledCertificateIdsRequest,
type OCPP20GetInstalledCertificateIdsResponse,
+ type OCPP20GetLogRequest,
+ type OCPP20GetLogResponse,
+ type OCPP20GetTransactionStatusRequest,
+ type OCPP20GetTransactionStatusResponse,
type OCPP20GetVariablesRequest,
type OCPP20GetVariablesResponse,
type OCPP20HeartbeatRequest,
OCPP20IncomingRequestCommand,
type OCPP20InstallCertificateRequest,
type OCPP20InstallCertificateResponse,
+ type OCPP20LogStatusNotificationRequest,
+ type OCPP20LogStatusNotificationResponse,
+ OCPP20MeasurandEnumType,
+ type OCPP20MeterValuesRequest,
+ type OCPP20MeterValuesResponse,
+ type OCPP20NotifyCustomerInformationRequest,
+ type OCPP20NotifyCustomerInformationResponse,
type OCPP20NotifyReportRequest,
type OCPP20NotifyReportResponse,
+ OCPP20ReadingContextEnumType,
OCPP20RequestCommand,
type OCPP20RequestStartTransactionRequest,
type OCPP20RequestStartTransactionResponse,
OCPP20RequiredVariableName,
type OCPP20ResetRequest,
type OCPP20ResetResponse,
+ type OCPP20SetNetworkProfileRequest,
+ type OCPP20SetNetworkProfileResponse,
type OCPP20SetVariablesRequest,
type OCPP20SetVariablesResponse,
type OCPP20StatusNotificationRequest,
type OCPP20TriggerMessageResponse,
type OCPP20UnlockConnectorRequest,
type OCPP20UnlockConnectorResponse,
+ type OCPP20UpdateFirmwareRequest,
+ type OCPP20UpdateFirmwareResponse,
OCPPVersion,
+ OperationalStatusEnumType,
ReasonCodeEnumType,
RegistrationStatusEnumType,
ReportBaseEnumType,
RequestStartStopStatusEnumType,
ResetEnumType,
ResetStatusEnumType,
+ SetNetworkProfileStatusEnumType,
SetVariableStatusEnumType,
StopTransactionReason,
TriggerMessageStatusEnumType,
UnlockStatusEnumType,
+ UpdateFirmwareStatusEnumType,
+ UploadLogStatusEnumType,
} from '../../../types/index.js'
import {
OCPP20ChargingProfileKindEnumType,
generateUUID,
isAsyncFunction,
logger,
+ sleep,
validateUUID,
} from '../../../utils/index.js'
import {
OCPP20IncomingRequestCommand.CERTIFICATE_SIGNED,
this.toHandler(this.handleRequestCertificateSigned.bind(this)),
],
+ [
+ OCPP20IncomingRequestCommand.CHANGE_AVAILABILITY,
+ this.toHandler(this.handleRequestChangeAvailability.bind(this)),
+ ],
[
OCPP20IncomingRequestCommand.CLEAR_CACHE,
this.toHandler(this.handleRequestClearCache.bind(this)),
],
+ [
+ OCPP20IncomingRequestCommand.CUSTOMER_INFORMATION,
+ this.toHandler(this.handleRequestCustomerInformation.bind(this)),
+ ],
+ [
+ OCPP20IncomingRequestCommand.DATA_TRANSFER,
+ this.toHandler(this.handleRequestDataTransfer.bind(this)),
+ ],
[
OCPP20IncomingRequestCommand.DELETE_CERTIFICATE,
this.toHandler(this.handleRequestDeleteCertificate.bind(this)),
OCPP20IncomingRequestCommand.GET_INSTALLED_CERTIFICATE_IDS,
this.toHandler(this.handleRequestGetInstalledCertificateIds.bind(this)),
],
+ [OCPP20IncomingRequestCommand.GET_LOG, this.toHandler(this.handleRequestGetLog.bind(this))],
+ [
+ OCPP20IncomingRequestCommand.GET_TRANSACTION_STATUS,
+ this.toHandler(this.handleRequestGetTransactionStatus.bind(this)),
+ ],
[
OCPP20IncomingRequestCommand.GET_VARIABLES,
this.toHandler(this.handleRequestGetVariables.bind(this)),
this.toHandler(this.handleRequestStopTransaction.bind(this)),
],
[OCPP20IncomingRequestCommand.RESET, this.toHandler(this.handleRequestReset.bind(this))],
+ [
+ OCPP20IncomingRequestCommand.SET_NETWORK_PROFILE,
+ this.toHandler(this.handleRequestSetNetworkProfile.bind(this)),
+ ],
[
OCPP20IncomingRequestCommand.SET_VARIABLES,
this.toHandler(this.handleRequestSetVariables.bind(this)),
OCPP20IncomingRequestCommand.UNLOCK_CONNECTOR,
this.toHandler(this.handleRequestUnlockConnector.bind(this)),
],
+ [
+ OCPP20IncomingRequestCommand.UPDATE_FIRMWARE,
+ this.toHandler(this.handleRequestUpdateFirmware.bind(this)),
+ ],
])
this.payloadValidatorFunctions = OCPP20ServiceUtils.createPayloadValidatorMap(
OCPP20ServiceUtils.createIncomingRequestPayloadConfigs(),
return secondsToMilliseconds(Constants.DEFAULT_TX_UPDATED_INTERVAL)
}
+ private handleCsLevelInoperative (
+ chargingStation: ChargingStation,
+ operationalStatus: OperationalStatusEnumType,
+ newConnectorStatus: OCPP20ConnectorStatusEnumType
+ ): OCPP20ChangeAvailabilityResponse | undefined {
+ let hasActiveTransactions = false
+ for (const [evseId, evseStatus] of chargingStation.evses) {
+ if (evseId === 0) {
+ continue
+ }
+ if (this.hasEvseActiveTransactions(evseStatus)) {
+ hasActiveTransactions = true
+ logger.info(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestChangeAvailability: EVSE ${evseId.toString()} has active transaction, will be set Inoperative when transaction ends`
+ )
+ } else {
+ evseStatus.availability = operationalStatus
+ logger.info(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestChangeAvailability: EVSE ${evseId.toString()} set to ${operationalStatus} immediately (idle)`
+ )
+ }
+ }
+ if (hasActiveTransactions) {
+ for (const [evseId, evseStatus] of chargingStation.evses) {
+ if (evseId > 0 && !this.hasEvseActiveTransactions(evseStatus)) {
+ this.sendEvseStatusNotifications(chargingStation, evseId, newConnectorStatus)
+ }
+ }
+ logger.info(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestChangeAvailability: Charging station partially set to ${operationalStatus}, some EVSEs scheduled`
+ )
+ return {
+ status: ChangeAvailabilityStatusEnumType.Scheduled,
+ }
+ }
+ return undefined
+ }
+
+ private handleEvseChangeAvailability (
+ chargingStation: ChargingStation,
+ evseId: number,
+ operationalStatus: OperationalStatusEnumType,
+ newConnectorStatus: OCPP20ConnectorStatusEnumType
+ ): OCPP20ChangeAvailabilityResponse {
+ if (!chargingStation.evses.has(evseId)) {
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestChangeAvailability: EVSE ${evseId.toString()} not found, rejecting`
+ )
+ return {
+ status: ChangeAvailabilityStatusEnumType.Rejected,
+ statusInfo: {
+ additionalInfo: `EVSE ${evseId.toString()} does not exist on charging station`,
+ reasonCode: ReasonCodeEnumType.UnknownEvse,
+ },
+ }
+ }
+
+ const evseStatus = chargingStation.getEvseStatus(evseId)
+ if (
+ evseStatus != null &&
+ operationalStatus === OperationalStatusEnumType.Inoperative &&
+ this.hasEvseActiveTransactions(evseStatus)
+ ) {
+ logger.info(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestChangeAvailability: EVSE ${evseId.toString()} has active transaction, scheduling availability change`
+ )
+ return {
+ status: ChangeAvailabilityStatusEnumType.Scheduled,
+ }
+ }
+
+ if (evseStatus != null) {
+ evseStatus.availability = operationalStatus
+ }
+ this.sendEvseStatusNotifications(chargingStation, evseId, newConnectorStatus)
+
+ logger.info(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestChangeAvailability: EVSE ${evseId.toString()} set to ${operationalStatus}`
+ )
+ return {
+ status: ChangeAvailabilityStatusEnumType.Accepted,
+ }
+ }
+
/**
* Handles OCPP 2.0 CertificateSigned request from central system
* Receives signed certificate chain from CSMS and stores it in the charging station
}
}
+ /**
+ * Handles OCPP 2.0.1 ChangeAvailability request from central system (F03, F04).
+ * Changes the operational status of the entire charging station or a specific EVSE.
+ * Per G03.FR.01: EVSE level without ongoing transaction → Accepted
+ * Per G03.FR.02: CS level without ongoing transaction → Accepted
+ * Per G03.FR.03: EVSE level with ongoing transaction and Inoperative → Scheduled
+ * Per G03.FR.04: CS level with some EVSEs having transactions and Inoperative → Scheduled
+ * @param chargingStation - The charging station instance processing the request
+ * @param commandPayload - ChangeAvailability request payload with operationalStatus and optional evse
+ * @returns ChangeAvailabilityResponse with Accepted, Rejected, or Scheduled
+ */
+ private handleRequestChangeAvailability (
+ chargingStation: ChargingStation,
+ commandPayload: OCPP20ChangeAvailabilityRequest
+ ): OCPP20ChangeAvailabilityResponse {
+ const { evse, operationalStatus } = commandPayload
+ const evseIdLabel = evse?.id == null ? '' : ` for EVSE ${evse.id.toString()}`
+
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestChangeAvailability: Received ChangeAvailability request with operationalStatus=${operationalStatus}${evseIdLabel}`
+ )
+
+ const newConnectorStatus =
+ operationalStatus === OperationalStatusEnumType.Inoperative
+ ? OCPP20ConnectorStatusEnumType.Unavailable
+ : OCPP20ConnectorStatusEnumType.Available
+
+ // EVSE-level change
+ if (evse?.id != null && evse.id > 0) {
+ return this.handleEvseChangeAvailability(
+ chargingStation,
+ evse.id,
+ operationalStatus,
+ newConnectorStatus
+ )
+ }
+
+ // CS-level change (no evse or evse.id === 0)
+ if (operationalStatus === OperationalStatusEnumType.Inoperative) {
+ const result = this.handleCsLevelInoperative(
+ chargingStation,
+ operationalStatus,
+ newConnectorStatus
+ )
+ if (result != null) {
+ return result
+ }
+ }
+
+ // Apply availability change to all EVSEs (for Operative, or Inoperative with no active transactions)
+ for (const [evseId, evseStatus] of chargingStation.evses) {
+ if (evseId > 0) {
+ evseStatus.availability = operationalStatus
+ }
+ }
+ this.sendAllConnectorsStatusNotifications(chargingStation, newConnectorStatus)
+
+ logger.info(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestChangeAvailability: Charging station set to ${operationalStatus}`
+ )
+ return {
+ status: ChangeAvailabilityStatusEnumType.Accepted,
+ }
+ }
+
+ /**
+ * Handles OCPP 2.0.1 CustomerInformation request from central system.
+ * Per TC_N_32_CS: CS must respond to CustomerInformation with Accepted for clear requests.
+ * Simulator has no persistent customer data, so clear is accepted but no-op.
+ * For report requests, sends empty NotifyCustomerInformation (simulator has no real data).
+ * @param chargingStation - The charging station instance processing the request
+ * @param commandPayload - CustomerInformation request payload with clear/report flags
+ * @returns CustomerInformationResponse with status
+ */
+ private handleRequestCustomerInformation (
+ chargingStation: ChargingStation,
+ commandPayload: OCPP20CustomerInformationRequest
+ ): OCPP20CustomerInformationResponse {
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestCustomerInformation: Received CustomerInformation request with clear=${commandPayload.clear.toString()}, report=${commandPayload.report.toString()}`
+ )
+
+ if (commandPayload.clear) {
+ logger.info(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestCustomerInformation: Clear request accepted (simulator has no persistent customer data)`
+ )
+ return {
+ status: CustomerInformationStatusEnumType.Accepted,
+ }
+ }
+
+ if (commandPayload.report) {
+ logger.info(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestCustomerInformation: Report request accepted, sending empty NotifyCustomerInformation`
+ )
+ // Fire-and-forget NotifyCustomerInformation with empty data
+ setImmediate(() => {
+ this.sendNotifyCustomerInformation(chargingStation, commandPayload.requestId).catch(
+ (error: unknown) => {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestCustomerInformation: Error sending NotifyCustomerInformation:`,
+ error
+ )
+ }
+ )
+ })
+ return {
+ status: CustomerInformationStatusEnumType.Accepted,
+ }
+ }
+
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestCustomerInformation: Neither clear nor report flag set, rejecting`
+ )
+ return {
+ status: CustomerInformationStatusEnumType.Rejected,
+ }
+ }
+
+ /**
+ * Handles OCPP 2.0.1 DataTransfer request
+ * Per TC_P_01_CS: CS with no vendor extensions must respond UnknownVendorId
+ * @param chargingStation - The charging station instance
+ * @param commandPayload - The DataTransfer request payload
+ * @returns DataTransferResponse with UnknownVendorId status
+ */
+ private handleRequestDataTransfer (
+ chargingStation: ChargingStation,
+ commandPayload: OCPP20DataTransferRequest
+ ): OCPP20DataTransferResponse {
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestDataTransfer: Received DataTransfer request with vendorId '${commandPayload.vendorId}'`
+ )
+ // Per TC_P_01_CS: CS with no vendor extensions must respond UnknownVendorId
+ return {
+ status: DataTransferStatusEnumType.UnknownVendorId,
+ }
+ }
+
/**
* Handles OCPP 2.0 DeleteCertificate request from central system
* Deletes a certificate matching the provided hash data from the charging station
}
}
+ /**
+ * Handles OCPP 2.0.1 GetLog request from central system.
+ * Accepts the log upload request and simulates the log upload lifecycle
+ * by sending LogStatusNotification messages through a state machine:
+ * Uploading → Uploaded
+ * @param chargingStation - The charging station instance processing the request
+ * @param commandPayload - GetLog request payload with log type, requestId, and log parameters
+ * @returns GetLogResponse with Accepted status and simulated filename
+ */
+ private handleRequestGetLog (
+ chargingStation: ChargingStation,
+ commandPayload: OCPP20GetLogRequest
+ ): OCPP20GetLogResponse {
+ const { logType, requestId } = commandPayload
+
+ logger.info(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetLog: Received GetLog request with requestId ${requestId.toString()} for logType '${logType}'`
+ )
+
+ // Fire-and-forget log upload state machine after response is returned
+ setImmediate(() => {
+ this.simulateLogUploadLifecycle(chargingStation, requestId).catch((error: unknown) => {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetLog: Error during log upload simulation:`,
+ error
+ )
+ })
+ })
+
+ return {
+ filename: 'simulator-log.txt',
+ status: LogStatusEnumType.Accepted,
+ }
+ }
+
+ /**
+ * Handles OCPP 2.0.1 GetTransactionStatus request from central system.
+ * Per D14, E28-E34: Returns transaction status with ongoingIndicator and messagesInQueue.
+ * @param chargingStation - The charging station instance processing the request
+ * @param commandPayload - GetTransactionStatus request payload with optional transactionId
+ * @returns GetTransactionStatusResponse with ongoingIndicator and messagesInQueue
+ */
+ private handleRequestGetTransactionStatus (
+ chargingStation: ChargingStation,
+ commandPayload: OCPP20GetTransactionStatusRequest
+ ): OCPP20GetTransactionStatusResponse {
+ const { transactionId } = commandPayload
+ const transactionLabel = transactionId == null ? '' : ` for transaction ID ${transactionId}`
+
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetTransactionStatus: Received GetTransactionStatus request${transactionLabel}`
+ )
+
+ // E14.FR.06: When transactionId is omitted, ongoingIndicator SHALL NOT be set
+ if (transactionId == null) {
+ return {
+ // Simulator has no persistent offline message buffer
+ messagesInQueue: false,
+ }
+ }
+
+ const evseId = chargingStation.getEvseIdByTransactionId(transactionId)
+
+ return {
+ // Simulator has no persistent offline message buffer
+ messagesInQueue: false,
+ ongoingIndicator: evseId != null,
+ }
+ }
+
private async handleRequestInstallCertificate (
chargingStation: ChargingStation,
commandPayload: OCPP20InstallCertificateRequest
}
}
+ /**
+ * Handles OCPP 2.0.1 SetNetworkProfile request from central system
+ * Per TC_B_43_CS: CS must respond to SetNetworkProfile at minimum with Rejected
+ * The simulator does not support network profile switching
+ * @param chargingStation - The charging station instance
+ * @param commandPayload - The SetNetworkProfile request payload
+ * @returns SetNetworkProfileResponse with Rejected status
+ */
+ private handleRequestSetNetworkProfile (
+ chargingStation: ChargingStation,
+ commandPayload: OCPP20SetNetworkProfileRequest
+ ): OCPP20SetNetworkProfileResponse {
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestSetNetworkProfile: Received SetNetworkProfile request`
+ )
+ // Per TC_B_43_CS: CS must respond to SetNetworkProfile at minimum with Rejected
+ return {
+ status: SetNetworkProfileStatusEnumType.Rejected,
+ statusInfo: {
+ additionalInfo: 'Simulator does not support network profile configuration',
+ reasonCode: ReasonCodeEnumType.UnsupportedRequest,
+ },
+ }
+ }
+
/**
* Handles OCPP 2.0 RequestStartTransaction request from central system
* Initiates charging transaction on specified EVSE with enhanced authorization
})
return { status: TriggerMessageStatusEnumType.Accepted }
+ case MessageTriggerEnumType.FirmwareStatusNotification:
+ chargingStation.ocppRequestService
+ .requestHandler<
+ OCPP20FirmwareStatusNotificationRequest,
+ OCPP20FirmwareStatusNotificationResponse
+ >(
+ chargingStation,
+ OCPP20RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
+ {
+ status: FirmwareStatusEnumType.Idle,
+ },
+ { skipBufferingOnError: true, triggerMessage: true }
+ )
+ .catch((error: unknown) => {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestTriggerMessage: Error sending FirmwareStatusNotification:`,
+ error
+ )
+ })
+ return { status: TriggerMessageStatusEnumType.Accepted }
+
case MessageTriggerEnumType.Heartbeat:
chargingStation.ocppRequestService
.requestHandler<
})
return { status: TriggerMessageStatusEnumType.Accepted }
+ case MessageTriggerEnumType.LogStatusNotification:
+ chargingStation.ocppRequestService
+ .requestHandler<
+ OCPP20LogStatusNotificationRequest,
+ OCPP20LogStatusNotificationResponse
+ >(
+ chargingStation,
+ OCPP20RequestCommand.LOG_STATUS_NOTIFICATION,
+ {
+ status: UploadLogStatusEnumType.Idle,
+ },
+ { skipBufferingOnError: true, triggerMessage: true }
+ )
+ .catch((error: unknown) => {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestTriggerMessage: Error sending LogStatusNotification:`,
+ error
+ )
+ })
+ return { status: TriggerMessageStatusEnumType.Accepted }
+
+ case MessageTriggerEnumType.MeterValues: {
+ const evseId = evse?.id ?? 0
+ chargingStation.ocppRequestService
+ .requestHandler<OCPP20MeterValuesRequest, OCPP20MeterValuesResponse>(
+ chargingStation,
+ OCPP20RequestCommand.METER_VALUES,
+ {
+ evseId,
+ meterValue: [
+ {
+ sampledValue: [
+ {
+ context: OCPP20ReadingContextEnumType.TRIGGER,
+ measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
+ value: 0,
+ },
+ ],
+ timestamp: new Date(),
+ },
+ ],
+ },
+ { skipBufferingOnError: true, triggerMessage: true }
+ )
+ .catch((error: unknown) => {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestTriggerMessage: Error sending MeterValues:`,
+ error
+ )
+ })
+ return { status: TriggerMessageStatusEnumType.Accepted }
+ }
+
case MessageTriggerEnumType.StatusNotification:
if (evse?.id !== undefined && evse.id > 0 && evse.connectorId !== undefined) {
const evseStatus = chargingStation.evses.get(evse.id)
}
}
+ /**
+ * Handles OCPP 2.0.1 UpdateFirmware request from central system.
+ * Accepts the firmware update request and simulates the firmware update lifecycle
+ * by sending FirmwareStatusNotification messages through a state machine:
+ * Downloading → Downloaded → [SignatureVerified] → Installing → Installed
+ * @param chargingStation - The charging station instance processing the request
+ * @param commandPayload - UpdateFirmware request payload with firmware details and requestId
+ * @returns UpdateFirmwareResponse with Accepted status
+ */
+ private handleRequestUpdateFirmware (
+ chargingStation: ChargingStation,
+ commandPayload: OCPP20UpdateFirmwareRequest
+ ): OCPP20UpdateFirmwareResponse {
+ const { firmware, requestId } = commandPayload
+
+ logger.info(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Received UpdateFirmware request with requestId ${requestId.toString()} for location '${firmware.location}'`
+ )
+
+ // Fire-and-forget firmware update state machine after response is returned
+ setImmediate(() => {
+ this.simulateFirmwareUpdateLifecycle(chargingStation, requestId, firmware.signature).catch(
+ (error: unknown) => {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Error during firmware update simulation:`,
+ error
+ )
+ }
+ )
+ })
+
+ return {
+ status: UpdateFirmwareStatusEnumType.Accepted,
+ }
+ }
+
/**
* Checks if a specific EVSE has any active transactions.
* @param evse - The EVSE to check
}
}
+ private sendFirmwareStatusNotification (
+ chargingStation: ChargingStation,
+ status: FirmwareStatusEnumType,
+ requestId: number
+ ): Promise<OCPP20FirmwareStatusNotificationResponse> {
+ return chargingStation.ocppRequestService.requestHandler<
+ OCPP20FirmwareStatusNotificationRequest,
+ OCPP20FirmwareStatusNotificationResponse
+ >(chargingStation, OCPP20RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
+ requestId,
+ status,
+ })
+ }
+
+ private sendLogStatusNotification (
+ chargingStation: ChargingStation,
+ status: UploadLogStatusEnumType,
+ requestId: number
+ ): Promise<OCPP20LogStatusNotificationResponse> {
+ return chargingStation.ocppRequestService.requestHandler<
+ OCPP20LogStatusNotificationRequest,
+ OCPP20LogStatusNotificationResponse
+ >(chargingStation, OCPP20RequestCommand.LOG_STATUS_NOTIFICATION, {
+ requestId,
+ status,
+ })
+ }
+
+ private async sendNotifyCustomerInformation (
+ chargingStation: ChargingStation,
+ requestId: number
+ ): Promise<void> {
+ const notifyCustomerInformationRequest: OCPP20NotifyCustomerInformationRequest = {
+ data: '',
+ generatedAt: new Date(),
+ requestId,
+ seqNo: 0,
+ tbc: false,
+ }
+ await chargingStation.ocppRequestService.requestHandler<
+ OCPP20NotifyCustomerInformationRequest,
+ OCPP20NotifyCustomerInformationResponse
+ >(
+ chargingStation,
+ OCPP20RequestCommand.NOTIFY_CUSTOMER_INFORMATION,
+ notifyCustomerInformationRequest
+ )
+ }
+
private async sendNotifyReportRequest (
chargingStation: ChargingStation,
request: OCPP20GetBaseReportRequest,
this.reportDataCache.delete(requestId)
}
+ /**
+ * Simulates a firmware update lifecycle through status progression using chained setTimeout calls.
+ * Sequence: Downloading → Downloaded → [SignatureVerified if signature present] → Installing → Installed
+ * @param chargingStation - The charging station instance
+ * @param requestId - The request ID from the UpdateFirmware request
+ * @param signature - Optional firmware signature; triggers SignatureVerified step if present
+ */
+ private async simulateFirmwareUpdateLifecycle (
+ chargingStation: ChargingStation,
+ requestId: number,
+ signature?: string
+ ): Promise<void> {
+ await this.sendFirmwareStatusNotification(
+ chargingStation,
+ FirmwareStatusEnumType.Downloading,
+ requestId
+ )
+
+ await sleep(1000)
+ await this.sendFirmwareStatusNotification(
+ chargingStation,
+ FirmwareStatusEnumType.Downloaded,
+ requestId
+ )
+
+ if (signature != null) {
+ await sleep(500)
+ await this.sendFirmwareStatusNotification(
+ chargingStation,
+ FirmwareStatusEnumType.SignatureVerified,
+ requestId
+ )
+ }
+
+ await sleep(1000)
+ await this.sendFirmwareStatusNotification(
+ chargingStation,
+ FirmwareStatusEnumType.Installing,
+ requestId
+ )
+
+ await sleep(1000)
+ await this.sendFirmwareStatusNotification(
+ chargingStation,
+ FirmwareStatusEnumType.Installed,
+ requestId
+ )
+
+ logger.info(
+ `${chargingStation.logPrefix()} ${moduleName}.simulateFirmwareUpdateLifecycle: Firmware update simulation completed for requestId ${requestId.toString()}`
+ )
+ }
+
+ /**
+ * Simulates a log upload lifecycle through status progression using chained setTimeout calls.
+ * Sequence: Uploading → Uploaded
+ * @param chargingStation - The charging station instance
+ * @param requestId - The request ID from the GetLog request
+ */
+ private async simulateLogUploadLifecycle (
+ chargingStation: ChargingStation,
+ requestId: number
+ ): Promise<void> {
+ await this.sendLogStatusNotification(
+ chargingStation,
+ UploadLogStatusEnumType.Uploading,
+ requestId
+ )
+
+ await sleep(1000)
+ await this.sendLogStatusNotification(
+ chargingStation,
+ UploadLogStatusEnumType.Uploaded,
+ requestId
+ )
+
+ logger.info(
+ `${chargingStation.logPrefix()} ${moduleName}.simulateLogUploadLifecycle: Log upload simulation completed for requestId ${requestId.toString()}`
+ )
+ }
+
/**
* Terminates all active transactions on the charging station using OCPP 2.0 TransactionEventRequest
* @param chargingStation - The charging station instance
type CertificateActionEnumType,
type CertificateSigningUseEnumType,
ErrorType,
+ type FirmwareStatusEnumType,
type JsonObject,
type JsonType,
+ type OCPP20FirmwareStatusNotificationRequest,
+ type OCPP20FirmwareStatusNotificationResponse,
type OCPP20Get15118EVCertificateRequest,
type OCPP20Get15118EVCertificateResponse,
type OCPP20GetCertificateStatusRequest,
type OCPP20GetCertificateStatusResponse,
+ type OCPP20LogStatusNotificationRequest,
+ type OCPP20LogStatusNotificationResponse,
+ type OCPP20MeterValue,
+ type OCPP20MeterValuesRequest,
+ type OCPP20MeterValuesResponse,
+ type OCPP20NotifyCustomerInformationRequest,
+ type OCPP20NotifyCustomerInformationResponse,
OCPP20RequestCommand,
+ type OCPP20SecurityEventNotificationRequest,
+ type OCPP20SecurityEventNotificationResponse,
type OCPP20SignCertificateRequest,
type OCPP20SignCertificateResponse,
OCPPVersion,
type OCSPRequestDataType,
type RequestParams,
+ type UploadLogStatusEnumType,
} from '../../../types/index.js'
import { generateUUID, logger } from '../../../utils/index.js'
import { OCPPRequestService } from '../OCPPRequestService.js'
this.buildRequestPayload = this.buildRequestPayload.bind(this)
}
+ /**
+ * Send a FirmwareStatusNotification to the CSMS.
+ *
+ * Notifies the CSMS about the progress of a firmware update on the charging station.
+ * Per OCPP 2.0.1 use case J01, the CS sends firmware status updates during the
+ * download, verification, and installation phases of a firmware update.
+ * The response is an empty object — the CSMS acknowledges receipt without data.
+ * @param chargingStation - The charging station reporting the firmware status
+ * @param status - Current firmware update status (e.g., Downloading, Installed)
+ * @param requestId - The request ID from the original UpdateFirmware request
+ * @returns Promise resolving to the empty CSMS acknowledgement response
+ */
+ public async requestFirmwareStatusNotification (
+ chargingStation: ChargingStation,
+ status: FirmwareStatusEnumType,
+ requestId?: number
+ ): Promise<OCPP20FirmwareStatusNotificationResponse> {
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.requestFirmwareStatusNotification: Sending FirmwareStatusNotification with status '${status}'`
+ )
+
+ const requestPayload: OCPP20FirmwareStatusNotificationRequest = {
+ status,
+ ...(requestId !== undefined && { requestId }),
+ }
+
+ const messageId = generateUUID()
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.requestFirmwareStatusNotification: Sending FirmwareStatusNotification request with message ID '${messageId}'`
+ )
+
+ const response = (await this.sendMessage(
+ chargingStation,
+ messageId,
+ requestPayload,
+ OCPP20RequestCommand.FIRMWARE_STATUS_NOTIFICATION
+ )) as OCPP20FirmwareStatusNotificationResponse
+
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.requestFirmwareStatusNotification: Received response`
+ )
+
+ return response
+ }
+
/**
* Request an ISO 15118 EV certificate from the CSMS.
*
throw new OCPPError(ErrorType.NOT_SUPPORTED, errorMsg, commandName, commandParams)
}
+ /**
+ * Send a LogStatusNotification to the CSMS.
+ *
+ * Notifies the CSMS about the progress of a log upload on the charging station.
+ * Per OCPP 2.0.1 use case M04, the CS sends log upload status updates during
+ * the upload process. The response is an empty object — the CSMS acknowledges
+ * receipt without data.
+ * @param chargingStation - The charging station reporting the log upload status
+ * @param status - Current log upload status (e.g., Uploading, Uploaded)
+ * @param requestId - The request ID from the original GetLog request
+ * @returns Promise resolving to the empty CSMS acknowledgement response
+ */
+ public async requestLogStatusNotification (
+ chargingStation: ChargingStation,
+ status: UploadLogStatusEnumType,
+ requestId?: number
+ ): Promise<OCPP20LogStatusNotificationResponse> {
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.requestLogStatusNotification: Sending LogStatusNotification with status '${status}'`
+ )
+
+ const requestPayload: OCPP20LogStatusNotificationRequest = {
+ status,
+ ...(requestId !== undefined && { requestId }),
+ }
+
+ const messageId = generateUUID()
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.requestLogStatusNotification: Sending LogStatusNotification request with message ID '${messageId}'`
+ )
+
+ const response = (await this.sendMessage(
+ chargingStation,
+ messageId,
+ requestPayload,
+ OCPP20RequestCommand.LOG_STATUS_NOTIFICATION
+ )) as OCPP20LogStatusNotificationResponse
+
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.requestLogStatusNotification: Received response`
+ )
+
+ return response
+ }
+
+ /**
+ * Send MeterValues to the CSMS.
+ *
+ * Reports meter values for a specific EVSE to the CSMS outside of a transaction context.
+ * Per OCPP 2.0.1, the charging station may send sampled meter values (e.g., energy, power,
+ * voltage, current) at configured intervals or upon trigger. Each meter value contains
+ * one or more sampled values all taken at the same point in time.
+ * The response is an empty object — the CSMS acknowledges receipt without data.
+ * @param chargingStation - The charging station reporting the meter values
+ * @param evseId - The EVSE identifier (0 for main power meter, >0 for specific EVSE)
+ * @param meterValue - Array of meter value objects, each containing timestamped sampled values
+ * @returns Promise resolving to the empty CSMS acknowledgement response
+ */
+ public async requestMeterValues (
+ chargingStation: ChargingStation,
+ evseId: number,
+ meterValue: OCPP20MeterValue[]
+ ): Promise<OCPP20MeterValuesResponse> {
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.requestMeterValues: Sending MeterValues for EVSE ${evseId.toString()}`
+ )
+
+ const requestPayload: OCPP20MeterValuesRequest = {
+ evseId,
+ meterValue,
+ }
+
+ const messageId = generateUUID()
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.requestMeterValues: Sending MeterValues request with message ID '${messageId}'`
+ )
+
+ const response = (await this.sendMessage(
+ chargingStation,
+ messageId,
+ requestPayload,
+ OCPP20RequestCommand.METER_VALUES
+ )) as OCPP20MeterValuesResponse
+
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.requestMeterValues: Received response`
+ )
+
+ return response
+ }
+
+ /**
+ * Send NotifyCustomerInformation to the CSMS.
+ *
+ * Notifies the CSMS about customer information availability.
+ * For the simulator, this sends empty customer data as no real customer
+ * information is stored (GDPR compliance).
+ * @param chargingStation - The charging station sending the notification
+ * @param requestId - The request ID from the original CustomerInformation request
+ * @param data - Customer information data (empty string for simulator)
+ * @param seqNo - Sequence number for the notification
+ * @param generatedAt - Timestamp when the data was generated
+ * @param tbc - To be continued flag (false for simulator)
+ * @returns Promise resolving when the notification is sent
+ */
+ public async requestNotifyCustomerInformation (
+ chargingStation: ChargingStation,
+ requestId: number,
+ data: string,
+ seqNo: number,
+ generatedAt: Date,
+ tbc: boolean
+ ): Promise<OCPP20NotifyCustomerInformationResponse> {
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.requestNotifyCustomerInformation: Sending NotifyCustomerInformation`
+ )
+
+ const requestPayload: OCPP20NotifyCustomerInformationRequest = {
+ data,
+ generatedAt,
+ requestId,
+ seqNo,
+ tbc,
+ }
+
+ const messageId = generateUUID()
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.requestNotifyCustomerInformation: Sending NotifyCustomerInformation request with message ID '${messageId}'`
+ )
+
+ const response = (await this.sendMessage(
+ chargingStation,
+ messageId,
+ requestPayload,
+ OCPP20RequestCommand.NOTIFY_CUSTOMER_INFORMATION
+ )) as OCPP20NotifyCustomerInformationResponse
+
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.requestNotifyCustomerInformation: Received response`
+ )
+
+ return response
+ }
+
+ /**
+ * Send a SecurityEventNotification to the CSMS.
+ *
+ * Notifies the CSMS about a security event that occurred at the charging station.
+ * Per OCPP 2.0.1 use case A04, the CS sends security events (e.g., tamper detection,
+ * firmware validation failure, invalid certificate) to keep the CSMS informed.
+ * The response is an empty object — the CSMS acknowledges receipt without data.
+ * @param chargingStation - The charging station reporting the security event
+ * @param type - Type of the security event (from the Security events list, max 50 chars)
+ * @param timestamp - Date and time at which the event occurred
+ * @param techInfo - Optional additional technical information about the event (max 255 chars)
+ * @returns Promise resolving to the empty CSMS acknowledgement response
+ */
+ public async requestSecurityEventNotification (
+ chargingStation: ChargingStation,
+ type: string,
+ timestamp: Date,
+ techInfo?: string
+ ): Promise<OCPP20SecurityEventNotificationResponse> {
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.requestSecurityEventNotification: Sending SecurityEventNotification`
+ )
+
+ const requestPayload: OCPP20SecurityEventNotificationRequest = {
+ timestamp,
+ type,
+ ...(techInfo !== undefined && { techInfo }),
+ }
+
+ const messageId = generateUUID()
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.requestSecurityEventNotification: Sending SecurityEventNotification request with message ID '${messageId}'`
+ )
+
+ const response = (await this.sendMessage(
+ chargingStation,
+ messageId,
+ requestPayload,
+ OCPP20RequestCommand.SECURITY_EVENT_NOTIFICATION
+ )) as OCPP20SecurityEventNotificationResponse
+
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.requestSecurityEventNotification: Received response`
+ )
+
+ return response
+ }
+
/**
* Request certificate signing from the CSMS.
*
switch (commandName) {
case OCPP20RequestCommand.BOOT_NOTIFICATION:
return commandParams as unknown as Request
+ case OCPP20RequestCommand.FIRMWARE_STATUS_NOTIFICATION:
+ case OCPP20RequestCommand.LOG_STATUS_NOTIFICATION:
+ case OCPP20RequestCommand.METER_VALUES:
+ case OCPP20RequestCommand.NOTIFY_CUSTOMER_INFORMATION:
+ case OCPP20RequestCommand.SECURITY_EVENT_NOTIFICATION:
+ return commandParams as unknown as Request
case OCPP20RequestCommand.HEARTBEAT:
return OCPP20Constants.OCPP_RESPONSE_EMPTY as unknown as Request
case OCPP20RequestCommand.NOTIFY_REPORT:
ErrorType,
type JsonType,
type OCPP20BootNotificationResponse,
+ type OCPP20FirmwareStatusNotificationResponse,
type OCPP20HeartbeatResponse,
OCPP20IncomingRequestCommand,
+ type OCPP20LogStatusNotificationResponse,
+ type OCPP20MeterValuesResponse,
+ type OCPP20NotifyCustomerInformationResponse,
type OCPP20NotifyReportResponse,
OCPP20OptionalVariableName,
OCPP20RequestCommand,
+ type OCPP20SecurityEventNotificationResponse,
type OCPP20StatusNotificationResponse,
+ type OCPP20TransactionEventRequest,
type OCPP20TransactionEventResponse,
OCPPVersion,
RegistrationStatusEnumType,
type ResponseHandler,
} from '../../../types/index.js'
+import { OCPP20AuthorizationStatusEnumType } from '../../../types/ocpp/2.0/Transaction.js'
import { isAsyncFunction, logger } from '../../../utils/index.js'
import { OCPPResponseService } from '../OCPPResponseService.js'
import { OCPP20ServiceUtils } from './OCPP20ServiceUtils.js'
OCPP20RequestCommand.BOOT_NOTIFICATION,
this.handleResponseBootNotification.bind(this) as ResponseHandler,
],
+ [
+ OCPP20RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
+ this.handleResponseFirmwareStatusNotification.bind(this) as ResponseHandler,
+ ],
[OCPP20RequestCommand.HEARTBEAT, this.handleResponseHeartbeat.bind(this) as ResponseHandler],
+ [
+ OCPP20RequestCommand.LOG_STATUS_NOTIFICATION,
+ this.handleResponseLogStatusNotification.bind(this) as ResponseHandler,
+ ],
+ [
+ OCPP20RequestCommand.METER_VALUES,
+ this.handleResponseMeterValues.bind(this) as ResponseHandler,
+ ],
+ [
+ OCPP20RequestCommand.NOTIFY_CUSTOMER_INFORMATION,
+ this.handleResponseNotifyCustomerInformation.bind(this) as ResponseHandler,
+ ],
[
OCPP20RequestCommand.NOTIFY_REPORT,
this.handleResponseNotifyReport.bind(this) as ResponseHandler,
],
+ [
+ OCPP20RequestCommand.SECURITY_EVENT_NOTIFICATION,
+ this.handleResponseSecurityEventNotification.bind(this) as ResponseHandler,
+ ],
[
OCPP20RequestCommand.STATUS_NOTIFICATION,
this.handleResponseStatusNotification.bind(this) as ResponseHandler,
}
}
+ private handleResponseFirmwareStatusNotification (
+ chargingStation: ChargingStation,
+ payload: OCPP20FirmwareStatusNotificationResponse
+ ): void {
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.handleResponseFirmwareStatusNotification: FirmwareStatusNotification response received successfully`
+ )
+ }
+
private handleResponseHeartbeat (
chargingStation: ChargingStation,
payload: OCPP20HeartbeatResponse
)
}
+ private handleResponseLogStatusNotification (
+ chargingStation: ChargingStation,
+ payload: OCPP20LogStatusNotificationResponse
+ ): void {
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.handleResponseLogStatusNotification: LogStatusNotification response received successfully`
+ )
+ }
+
+ private handleResponseMeterValues (
+ chargingStation: ChargingStation,
+ payload: OCPP20MeterValuesResponse
+ ): void {
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.handleResponseMeterValues: MeterValues response received successfully`
+ )
+ }
+
+ private handleResponseNotifyCustomerInformation (
+ chargingStation: ChargingStation,
+ payload: OCPP20NotifyCustomerInformationResponse
+ ): void {
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.handleResponseNotifyCustomerInformation: NotifyCustomerInformation response received successfully`
+ )
+ }
+
private handleResponseNotifyReport (
chargingStation: ChargingStation,
payload: OCPP20NotifyReportResponse
)
}
+ private handleResponseSecurityEventNotification (
+ chargingStation: ChargingStation,
+ payload: OCPP20SecurityEventNotificationResponse
+ ): void {
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.handleResponseSecurityEventNotification: SecurityEventNotification response received successfully`
+ )
+ }
+
private handleResponseStatusNotification (
chargingStation: ChargingStation,
payload: OCPP20StatusNotificationResponse
)
}
- // TODO: currently log-only — future work should act on idTokenInfo.status (Invalid/Blocked → stop transaction)
- // and chargingPriority (update charging profile priority) per OCPP 2.0.1 spec
+ /**
+ * Handles TransactionEvent response from CSMS.
+ *
+ * Per OCPP 2.0.1 spec (D01, D05): If the Charging Station started a transaction based on
+ * local authorization, but receives an Invalid, Blocked, Expired, or NoCredit status in the
+ * TransactionEventResponse idTokenInfo, the Charging Station SHALL stop the transaction.
+ * @param chargingStation - The charging station instance
+ * @param payload - The TransactionEvent response payload from CSMS
+ * @param requestPayload - The original TransactionEvent request payload
+ */
private handleResponseTransactionEvent (
chargingStation: ChargingStation,
- payload: OCPP20TransactionEventResponse
+ payload: OCPP20TransactionEventResponse,
+ requestPayload: OCPP20TransactionEventRequest
): void {
logger.debug(
`${chargingStation.logPrefix()} ${moduleName}.handleResponseTransactionEvent: TransactionEvent response received`
logger.info(
`${chargingStation.logPrefix()} ${moduleName}.handleResponseTransactionEvent: IdToken info status: ${payload.idTokenInfo.status}`
)
+ // D01/D05: Stop transaction when idToken authorization is rejected by CSMS
+ const rejectedStatuses = new Set<OCPP20AuthorizationStatusEnumType>([
+ OCPP20AuthorizationStatusEnumType.Blocked,
+ OCPP20AuthorizationStatusEnumType.Expired,
+ OCPP20AuthorizationStatusEnumType.Invalid,
+ OCPP20AuthorizationStatusEnumType.NoCredit,
+ ])
+ if (rejectedStatuses.has(payload.idTokenInfo.status)) {
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.handleResponseTransactionEvent: IdToken authorization rejected with status '${payload.idTokenInfo.status}', stopping active transaction per OCPP 2.0.1 spec (D01/D05)`
+ )
+ // Find the specific connector for this transaction
+ const connectorId = chargingStation.getConnectorIdByTransactionId(
+ requestPayload.transactionInfo.transactionId
+ )
+ const evseId = chargingStation.getEvseIdByTransactionId(
+ requestPayload.transactionInfo.transactionId
+ )
+ if (connectorId != null && evseId != null) {
+ logger.info(
+ `${chargingStation.logPrefix()} ${moduleName}.handleResponseTransactionEvent: Stopping transaction ${requestPayload.transactionInfo.transactionId} on EVSE ${evseId.toString()}, connector ${connectorId.toString()} due to rejected idToken`
+ )
+ OCPP20ServiceUtils.requestStopTransaction(chargingStation, connectorId, evseId).catch(
+ (error: unknown) => {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.handleResponseTransactionEvent: Error stopping transaction ${requestPayload.transactionInfo.transactionId} on connector ${connectorId.toString()}:`,
+ error
+ )
+ }
+ )
+ } else {
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.handleResponseTransactionEvent: Could not find connector for transaction ${requestPayload.transactionInfo.transactionId}, cannot stop transaction`
+ )
+ }
+ }
}
if (payload.updatedPersonalMessage != null) {
logger.info(
const moduleName = 'OCPP20ServiceUtils'
export class OCPP20ServiceUtils extends OCPPServiceUtils {
+ private static readonly incomingRequestSchemaNames: readonly [
+ OCPP20IncomingRequestCommand,
+ string
+ ][] = [
+ [OCPP20IncomingRequestCommand.CERTIFICATE_SIGNED, 'CertificateSigned'],
+ [OCPP20IncomingRequestCommand.CHANGE_AVAILABILITY, 'ChangeAvailability'],
+ [OCPP20IncomingRequestCommand.CLEAR_CACHE, 'ClearCache'],
+ [OCPP20IncomingRequestCommand.CUSTOMER_INFORMATION, 'CustomerInformation'],
+ [OCPP20IncomingRequestCommand.DATA_TRANSFER, 'DataTransfer'],
+ [OCPP20IncomingRequestCommand.DELETE_CERTIFICATE, 'DeleteCertificate'],
+ [OCPP20IncomingRequestCommand.GET_BASE_REPORT, 'GetBaseReport'],
+ [OCPP20IncomingRequestCommand.GET_INSTALLED_CERTIFICATE_IDS, 'GetInstalledCertificateIds'],
+ [OCPP20IncomingRequestCommand.GET_LOG, 'GetLog'],
+ [OCPP20IncomingRequestCommand.GET_TRANSACTION_STATUS, 'GetTransactionStatus'],
+ [OCPP20IncomingRequestCommand.GET_VARIABLES, 'GetVariables'],
+ [OCPP20IncomingRequestCommand.INSTALL_CERTIFICATE, 'InstallCertificate'],
+ [OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION, 'RequestStartTransaction'],
+ [OCPP20IncomingRequestCommand.REQUEST_STOP_TRANSACTION, 'RequestStopTransaction'],
+ [OCPP20IncomingRequestCommand.RESET, 'Reset'],
+ [OCPP20IncomingRequestCommand.SET_NETWORK_PROFILE, 'SetNetworkProfile'],
+ [OCPP20IncomingRequestCommand.SET_VARIABLES, 'SetVariables'],
+ [OCPP20IncomingRequestCommand.TRIGGER_MESSAGE, 'TriggerMessage'],
+ [OCPP20IncomingRequestCommand.UNLOCK_CONNECTOR, 'UnlockConnector'],
+ [OCPP20IncomingRequestCommand.UPDATE_FIRMWARE, 'UpdateFirmware'],
+ ]
+
+ private static readonly outgoingRequestSchemaNames: readonly [OCPP20RequestCommand, string][] = [
+ [OCPP20RequestCommand.BOOT_NOTIFICATION, 'BootNotification'],
+ [OCPP20RequestCommand.FIRMWARE_STATUS_NOTIFICATION, 'FirmwareStatusNotification'],
+ [OCPP20RequestCommand.HEARTBEAT, 'Heartbeat'],
+ [OCPP20RequestCommand.LOG_STATUS_NOTIFICATION, 'LogStatusNotification'],
+ [OCPP20RequestCommand.METER_VALUES, 'MeterValues'],
+ [OCPP20RequestCommand.NOTIFY_CUSTOMER_INFORMATION, 'NotifyCustomerInformation'],
+ [OCPP20RequestCommand.NOTIFY_REPORT, 'NotifyReport'],
+ [OCPP20RequestCommand.SECURITY_EVENT_NOTIFICATION, 'SecurityEventNotification'],
+ [OCPP20RequestCommand.STATUS_NOTIFICATION, 'StatusNotification'],
+ [OCPP20RequestCommand.TRANSACTION_EVENT, 'TransactionEvent'],
+ ]
+
/**
* Build a TransactionEvent request according to OCPP 2.0.1 specification
*
public static createIncomingRequestPayloadConfigs = (): [
OCPP20IncomingRequestCommand,
{ schemaPath: string }
- ][] => [
- [
- OCPP20IncomingRequestCommand.CERTIFICATE_SIGNED,
- OCPP20ServiceUtils.PayloadValidatorConfig('CertificateSignedRequest.json'),
- ],
- [
- OCPP20IncomingRequestCommand.CLEAR_CACHE,
- OCPP20ServiceUtils.PayloadValidatorConfig('ClearCacheRequest.json'),
- ],
- [
- OCPP20IncomingRequestCommand.DELETE_CERTIFICATE,
- OCPP20ServiceUtils.PayloadValidatorConfig('DeleteCertificateRequest.json'),
- ],
- [
- OCPP20IncomingRequestCommand.GET_BASE_REPORT,
- OCPP20ServiceUtils.PayloadValidatorConfig('GetBaseReportRequest.json'),
- ],
- [
- OCPP20IncomingRequestCommand.GET_INSTALLED_CERTIFICATE_IDS,
- OCPP20ServiceUtils.PayloadValidatorConfig('GetInstalledCertificateIdsRequest.json'),
- ],
- [
- OCPP20IncomingRequestCommand.GET_VARIABLES,
- OCPP20ServiceUtils.PayloadValidatorConfig('GetVariablesRequest.json'),
- ],
- [
- OCPP20IncomingRequestCommand.INSTALL_CERTIFICATE,
- OCPP20ServiceUtils.PayloadValidatorConfig('InstallCertificateRequest.json'),
- ],
- [
- OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION,
- OCPP20ServiceUtils.PayloadValidatorConfig('RequestStartTransactionRequest.json'),
- ],
- [
- OCPP20IncomingRequestCommand.REQUEST_STOP_TRANSACTION,
- OCPP20ServiceUtils.PayloadValidatorConfig('RequestStopTransactionRequest.json'),
- ],
- [
- OCPP20IncomingRequestCommand.RESET,
- OCPP20ServiceUtils.PayloadValidatorConfig('ResetRequest.json'),
- ],
- [
- OCPP20IncomingRequestCommand.SET_VARIABLES,
- OCPP20ServiceUtils.PayloadValidatorConfig('SetVariablesRequest.json'),
- ],
- [
- OCPP20IncomingRequestCommand.TRIGGER_MESSAGE,
- OCPP20ServiceUtils.PayloadValidatorConfig('TriggerMessageRequest.json'),
- ],
- [
- OCPP20IncomingRequestCommand.UNLOCK_CONNECTOR,
- OCPP20ServiceUtils.PayloadValidatorConfig('UnlockConnectorRequest.json'),
- ],
- ]
+ ][] =>
+ OCPP20ServiceUtils.incomingRequestSchemaNames.map(([command, schemaBase]) => [
+ command,
+ OCPP20ServiceUtils.PayloadValidatorConfig(`${schemaBase}Request.json`),
+ ])
/**
* Factory options for OCPP 2.0 Incoming Request Service
public static createIncomingRequestResponsePayloadConfigs = (): [
OCPP20IncomingRequestCommand,
{ schemaPath: string }
- ][] => [
- [
- OCPP20IncomingRequestCommand.CERTIFICATE_SIGNED,
- OCPP20ServiceUtils.PayloadValidatorConfig('CertificateSignedResponse.json'),
- ],
- [
- OCPP20IncomingRequestCommand.CLEAR_CACHE,
- OCPP20ServiceUtils.PayloadValidatorConfig('ClearCacheResponse.json'),
- ],
- [
- OCPP20IncomingRequestCommand.DELETE_CERTIFICATE,
- OCPP20ServiceUtils.PayloadValidatorConfig('DeleteCertificateResponse.json'),
- ],
- [
- OCPP20IncomingRequestCommand.GET_BASE_REPORT,
- OCPP20ServiceUtils.PayloadValidatorConfig('GetBaseReportResponse.json'),
- ],
- [
- OCPP20IncomingRequestCommand.GET_INSTALLED_CERTIFICATE_IDS,
- OCPP20ServiceUtils.PayloadValidatorConfig('GetInstalledCertificateIdsResponse.json'),
- ],
- [
- OCPP20IncomingRequestCommand.GET_VARIABLES,
- OCPP20ServiceUtils.PayloadValidatorConfig('GetVariablesResponse.json'),
- ],
- [
- OCPP20IncomingRequestCommand.INSTALL_CERTIFICATE,
- OCPP20ServiceUtils.PayloadValidatorConfig('InstallCertificateResponse.json'),
- ],
- [
- OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION,
- OCPP20ServiceUtils.PayloadValidatorConfig('RequestStartTransactionResponse.json'),
- ],
- [
- OCPP20IncomingRequestCommand.REQUEST_STOP_TRANSACTION,
- OCPP20ServiceUtils.PayloadValidatorConfig('RequestStopTransactionResponse.json'),
- ],
- [
- OCPP20IncomingRequestCommand.RESET,
- OCPP20ServiceUtils.PayloadValidatorConfig('ResetResponse.json'),
- ],
- [
- OCPP20IncomingRequestCommand.SET_VARIABLES,
- OCPP20ServiceUtils.PayloadValidatorConfig('SetVariablesResponse.json'),
- ],
- [
- OCPP20IncomingRequestCommand.TRIGGER_MESSAGE,
- OCPP20ServiceUtils.PayloadValidatorConfig('TriggerMessageResponse.json'),
- ],
- [
- OCPP20IncomingRequestCommand.UNLOCK_CONNECTOR,
- OCPP20ServiceUtils.PayloadValidatorConfig('UnlockConnectorResponse.json'),
- ],
- ]
+ ][] =>
+ OCPP20ServiceUtils.incomingRequestSchemaNames.map(([command, schemaBase]) => [
+ command,
+ OCPP20ServiceUtils.PayloadValidatorConfig(`${schemaBase}Response.json`),
+ ])
/**
* Factory options for OCPP 2.0 Incoming Request Response Service
public static createRequestPayloadConfigs = (): [
OCPP20RequestCommand,
{ schemaPath: string }
- ][] => [
- [
- OCPP20RequestCommand.BOOT_NOTIFICATION,
- OCPP20ServiceUtils.PayloadValidatorConfig('BootNotificationRequest.json'),
- ],
- [
- OCPP20RequestCommand.HEARTBEAT,
- OCPP20ServiceUtils.PayloadValidatorConfig('HeartbeatRequest.json'),
- ],
- [
- OCPP20RequestCommand.NOTIFY_REPORT,
- OCPP20ServiceUtils.PayloadValidatorConfig('NotifyReportRequest.json'),
- ],
- [
- OCPP20RequestCommand.STATUS_NOTIFICATION,
- OCPP20ServiceUtils.PayloadValidatorConfig('StatusNotificationRequest.json'),
- ],
- [
- OCPP20RequestCommand.TRANSACTION_EVENT,
- OCPP20ServiceUtils.PayloadValidatorConfig('TransactionEventRequest.json'),
- ],
- ]
+ ][] =>
+ OCPP20ServiceUtils.outgoingRequestSchemaNames.map(([command, schemaBase]) => [
+ command,
+ OCPP20ServiceUtils.PayloadValidatorConfig(`${schemaBase}Request.json`),
+ ])
/**
* Factory options for OCPP 2.0 Request Service
public static createResponsePayloadConfigs = (): [
OCPP20RequestCommand,
{ schemaPath: string }
- ][] => [
- [
- OCPP20RequestCommand.BOOT_NOTIFICATION,
- OCPP20ServiceUtils.PayloadValidatorConfig('BootNotificationResponse.json'),
- ],
- [
- OCPP20RequestCommand.HEARTBEAT,
- OCPP20ServiceUtils.PayloadValidatorConfig('HeartbeatResponse.json'),
- ],
- [
- OCPP20RequestCommand.NOTIFY_REPORT,
- OCPP20ServiceUtils.PayloadValidatorConfig('NotifyReportResponse.json'),
- ],
- [
- OCPP20RequestCommand.STATUS_NOTIFICATION,
- OCPP20ServiceUtils.PayloadValidatorConfig('StatusNotificationResponse.json'),
- ],
- [
- OCPP20RequestCommand.TRANSACTION_EVENT,
- OCPP20ServiceUtils.PayloadValidatorConfig('TransactionEventResponse.json'),
- ],
- ]
+ ][] =>
+ OCPP20ServiceUtils.outgoingRequestSchemaNames.map(([command, schemaBase]) => [
+ command,
+ OCPP20ServiceUtils.PayloadValidatorConfig(`${schemaBase}Response.json`),
+ ])
/**
* Factory options for OCPP 2.0 Response Service
import type {
CertificateActionEnumType,
CertificateSigningUseEnumType,
+ FirmwareStatusEnumType,
JsonType,
+ OCPP20FirmwareStatusNotificationResponse,
OCPP20Get15118EVCertificateResponse,
OCPP20GetCertificateStatusResponse,
+ OCPP20LogStatusNotificationResponse,
+ OCPP20MeterValue,
+ OCPP20MeterValuesResponse,
OCPP20RequestCommand,
+ OCPP20SecurityEventNotificationResponse,
OCPP20SignCertificateResponse,
OCSPRequestDataType,
RequestParams,
+ UploadLogStatusEnumType,
} from '../../../../types/index.js'
import type { ChargingStation } from '../../../index.js'
commandParams?: JsonType
) => JsonType
+ /**
+ * Send a FirmwareStatusNotification to the CSMS.
+ * Reports firmware update progress to the CSMS.
+ */
+ requestFirmwareStatusNotification: (
+ chargingStation: ChargingStation,
+ status: FirmwareStatusEnumType,
+ requestId?: number
+ ) => Promise<OCPP20FirmwareStatusNotificationResponse>
+
/**
* Request an ISO 15118 EV certificate from the CSMS.
* Forwards EXI-encoded certificate request from EV to CSMS.
chargingStation: ChargingStation,
ocspRequestData: OCSPRequestDataType
) => Promise<OCPP20GetCertificateStatusResponse>
+
+ /**
+ * Send a LogStatusNotification to the CSMS.
+ * Reports the status of a log upload initiated by a GetLog request.
+ */
+ requestLogStatusNotification: (
+ chargingStation: ChargingStation,
+ status: UploadLogStatusEnumType,
+ requestId?: number
+ ) => Promise<OCPP20LogStatusNotificationResponse>
+
+ /**
+ * Send MeterValues to the CSMS.
+ * Reports meter values for a specific EVSE outside of a transaction context.
+ */
+ requestMeterValues: (
+ chargingStation: ChargingStation,
+ evseId: number,
+ meterValue: OCPP20MeterValue[]
+ ) => Promise<OCPP20MeterValuesResponse>
+ /**
+ * Send a SecurityEventNotification to the CSMS.
+ * Notifies the CSMS about a security event at the charging station (A04).
+ */
+ requestSecurityEventNotification: (
+ chargingStation: ChargingStation,
+ type: string,
+ timestamp: Date,
+ techInfo?: string
+ ) => Promise<OCPP20SecurityEventNotificationResponse>
/**
* Request certificate signing from the CSMS.
* Generates a CSR and sends it to CSMS for signing.
import type {
OCPP20CertificateSignedRequest,
OCPP20CertificateSignedResponse,
+ OCPP20ChangeAvailabilityRequest,
+ OCPP20ChangeAvailabilityResponse,
OCPP20ClearCacheResponse,
+ OCPP20CustomerInformationRequest,
+ OCPP20CustomerInformationResponse,
+ OCPP20DataTransferRequest,
+ OCPP20DataTransferResponse,
OCPP20DeleteCertificateRequest,
OCPP20DeleteCertificateResponse,
OCPP20GetBaseReportRequest,
OCPP20GetBaseReportResponse,
OCPP20GetInstalledCertificateIdsRequest,
OCPP20GetInstalledCertificateIdsResponse,
+ OCPP20GetLogRequest,
+ OCPP20GetLogResponse,
+ OCPP20GetTransactionStatusRequest,
+ OCPP20GetTransactionStatusResponse,
OCPP20GetVariablesRequest,
OCPP20GetVariablesResponse,
OCPP20InstallCertificateRequest,
OCPP20RequestStopTransactionResponse,
OCPP20ResetRequest,
OCPP20ResetResponse,
+ OCPP20SetNetworkProfileRequest,
+ OCPP20SetNetworkProfileResponse,
OCPP20SetVariablesRequest,
OCPP20SetVariablesResponse,
OCPP20TriggerMessageRequest,
OCPP20TriggerMessageResponse,
OCPP20UnlockConnectorRequest,
OCPP20UnlockConnectorResponse,
+ OCPP20UpdateFirmwareRequest,
+ OCPP20UpdateFirmwareResponse,
ReportBaseEnumType,
ReportDataType,
} from '../../../../types/index.js'
) => ReportDataType[]
/**
- * Handles OCPP 2.0 CertificateSigned request from central system.
+ * Handles OCPP 2.0.1 CertificateSigned request from central system.
* Receives signed certificate chain from CSMS and stores it in the charging station.
*/
handleRequestCertificateSigned: (
commandPayload: OCPP20CertificateSignedRequest
) => Promise<OCPP20CertificateSignedResponse>
+ /**
+ * Handles OCPP 2.0.1 ChangeAvailability request from central system.
+ * Changes operational status of the entire charging station or a specific EVSE.
+ */
+ handleRequestChangeAvailability: (
+ chargingStation: ChargingStation,
+ commandPayload: OCPP20ChangeAvailabilityRequest
+ ) => OCPP20ChangeAvailabilityResponse
+
/**
* Handles OCPP 2.0.1 ClearCache request by clearing the Authorization Cache.
* Per C11.FR.04: Returns Rejected if AuthCacheEnabled is false.
*/
handleRequestClearCache: (chargingStation: ChargingStation) => Promise<OCPP20ClearCacheResponse>
+ /**
+ * Handles OCPP 2.0.1 CustomerInformation request from central system.
+ * Per TC_N_32_CS: CS must respond to CustomerInformation with Accepted for clear requests.
+ */
+ handleRequestCustomerInformation: (
+ chargingStation: ChargingStation,
+ commandPayload: OCPP20CustomerInformationRequest
+ ) => OCPP20CustomerInformationResponse
+
+ /**
+ * Handles OCPP 2.0.1 DataTransfer request.
+ * Per TC_P_01_CS: CS with no vendor extensions must respond UnknownVendorId.
+ */
+ handleRequestDataTransfer: (
+ chargingStation: ChargingStation,
+ commandPayload: OCPP20DataTransferRequest
+ ) => OCPP20DataTransferResponse
+
/**
* Handles OCPP 2.0 DeleteCertificate request from central system.
* Deletes a certificate matching the provided hash data from the charging station.
commandPayload: OCPP20GetInstalledCertificateIdsRequest
) => Promise<OCPP20GetInstalledCertificateIdsResponse>
+ /**
+ * Handles OCPP 2.0.1 GetLog request from central system.
+ * Accepts log upload and simulates upload lifecycle.
+ */
+ handleRequestGetLog: (
+ chargingStation: ChargingStation,
+ commandPayload: OCPP20GetLogRequest
+ ) => OCPP20GetLogResponse
+
+ /**
+ * Handles OCPP 2.0.1 GetTransactionStatus request from central system.
+ * Returns transaction status with ongoingIndicator and messagesInQueue.
+ */
+ handleRequestGetTransactionStatus: (
+ chargingStation: ChargingStation,
+ commandPayload: OCPP20GetTransactionStatusRequest
+ ) => OCPP20GetTransactionStatusResponse
+
/**
* Handles OCPP 2.0 GetVariables request.
* Returns values for requested variables from the device model.
commandPayload: OCPP20ResetRequest
) => Promise<OCPP20ResetResponse>
+ /**
+ * Handles OCPP 2.0.1 SetNetworkProfile request from central system.
+ * Per TC_B_43_CS: CS must respond to SetNetworkProfile at minimum with Rejected.
+ */
+ handleRequestSetNetworkProfile: (
+ chargingStation: ChargingStation,
+ commandPayload: OCPP20SetNetworkProfileRequest
+ ) => OCPP20SetNetworkProfileResponse
+
/**
* Handles OCPP 2.0 SetVariables request.
* Sets values for requested variables in the device model.
chargingStation: ChargingStation,
commandPayload: OCPP20UnlockConnectorRequest
) => Promise<OCPP20UnlockConnectorResponse>
+
+ handleRequestUpdateFirmware: (
+ chargingStation: ChargingStation,
+ commandPayload: OCPP20UpdateFirmwareRequest
+ ) => OCPP20UpdateFirmwareResponse
}
/**
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
buildReportData: (serviceImpl as any).buildReportData.bind(service),
handleRequestCertificateSigned: serviceImpl.handleRequestCertificateSigned.bind(service),
+ handleRequestChangeAvailability: serviceImpl.handleRequestChangeAvailability.bind(service),
handleRequestClearCache: serviceImpl.handleRequestClearCache.bind(service),
+ handleRequestCustomerInformation: serviceImpl.handleRequestCustomerInformation.bind(service),
+ handleRequestDataTransfer: serviceImpl.handleRequestDataTransfer.bind(service),
handleRequestDeleteCertificate: serviceImpl.handleRequestDeleteCertificate.bind(service),
handleRequestGetBaseReport: serviceImpl.handleRequestGetBaseReport.bind(service),
handleRequestGetInstalledCertificateIds:
serviceImpl.handleRequestGetInstalledCertificateIds.bind(service),
+ handleRequestGetLog: serviceImpl.handleRequestGetLog.bind(service),
+ handleRequestGetTransactionStatus: serviceImpl.handleRequestGetTransactionStatus.bind(service),
handleRequestGetVariables: serviceImpl.handleRequestGetVariables.bind(service),
handleRequestInstallCertificate: serviceImpl.handleRequestInstallCertificate.bind(service),
handleRequestReset: serviceImpl.handleRequestReset.bind(service),
+ handleRequestSetNetworkProfile: serviceImpl.handleRequestSetNetworkProfile.bind(service),
handleRequestSetVariables: serviceImpl.handleRequestSetVariables.bind(service),
handleRequestStartTransaction: serviceImpl.handleRequestStartTransaction.bind(service),
handleRequestStopTransaction: serviceImpl.handleRequestStopTransaction.bind(service),
handleRequestTriggerMessage: serviceImpl.handleRequestTriggerMessage.bind(service),
handleRequestUnlockConnector: serviceImpl.handleRequestUnlockConnector.bind(service),
+ handleRequestUpdateFirmware: serviceImpl.handleRequestUpdateFirmware.bind(service),
}
}
type CertificateHashDataChainType,
type CertificateHashDataType,
CertificateSigningUseEnumType,
+ ChangeAvailabilityStatusEnumType,
type CustomDataType,
+ CustomerInformationStatusEnumType,
DataEnumType,
+ DataTransferStatusEnumType,
DeleteCertificateStatusEnumType,
+ FirmwareStatusEnumType,
+ type FirmwareType,
GenericDeviceModelStatusEnumType,
GetCertificateIdUseEnumType,
GetCertificateStatusEnumType,
InstallCertificateStatusEnumType,
InstallCertificateUseEnumType,
Iso15118EVCertificateStatusEnumType,
+ LogEnumType,
+ type LogParametersType,
+ LogStatusEnumType,
MessageTriggerEnumType,
+ type NetworkConnectionProfileType,
OCPP20ComponentName,
OCPP20UnitEnumType,
type OCSPRequestDataType,
+ OperationalStatusEnumType,
ReasonCodeEnumType,
ReportBaseEnumType,
ResetEnumType,
ResetStatusEnumType,
+ SetNetworkProfileStatusEnumType,
TriggerMessageStatusEnumType,
UnlockStatusEnumType,
+ UpdateFirmwareStatusEnumType,
+ UploadLogStatusEnumType,
} from './ocpp/2.0/Common.js'
export {
OCPP20LocationEnumType,
export {
type OCPP20BootNotificationRequest,
type OCPP20CertificateSignedRequest,
+ type OCPP20ChangeAvailabilityRequest,
type OCPP20ClearCacheRequest,
+ type OCPP20CustomerInformationRequest,
+ type OCPP20DataTransferRequest,
type OCPP20DeleteCertificateRequest,
+ type OCPP20FirmwareStatusNotificationRequest,
type OCPP20Get15118EVCertificateRequest,
type OCPP20GetBaseReportRequest,
type OCPP20GetCertificateStatusRequest,
type OCPP20GetInstalledCertificateIdsRequest,
+ type OCPP20GetLogRequest,
+ type OCPP20GetTransactionStatusRequest,
type OCPP20GetVariablesRequest,
type OCPP20HeartbeatRequest,
OCPP20IncomingRequestCommand,
type OCPP20InstallCertificateRequest,
+ type OCPP20LogStatusNotificationRequest,
+ type OCPP20NotifyCustomerInformationRequest,
type OCPP20NotifyReportRequest,
OCPP20RequestCommand,
type OCPP20RequestStartTransactionRequest,
type OCPP20RequestStopTransactionRequest,
type OCPP20ResetRequest,
+ type OCPP20SecurityEventNotificationRequest,
+ type OCPP20SetNetworkProfileRequest,
type OCPP20SetVariablesRequest,
type OCPP20SignCertificateRequest,
type OCPP20StatusNotificationRequest,
type OCPP20TriggerMessageRequest,
type OCPP20UnlockConnectorRequest,
+ type OCPP20UpdateFirmwareRequest,
} from './ocpp/2.0/Requests.js'
export type {
OCPP20BootNotificationResponse,
OCPP20CertificateSignedResponse,
+ OCPP20ChangeAvailabilityResponse,
OCPP20ClearCacheResponse,
+ OCPP20CustomerInformationResponse,
+ OCPP20DataTransferResponse,
OCPP20DeleteCertificateResponse,
+ OCPP20FirmwareStatusNotificationResponse,
OCPP20Get15118EVCertificateResponse,
OCPP20GetBaseReportResponse,
OCPP20GetCertificateStatusResponse,
OCPP20GetInstalledCertificateIdsResponse,
+ OCPP20GetLogResponse,
+ OCPP20GetTransactionStatusResponse,
OCPP20GetVariablesResponse,
OCPP20HeartbeatResponse,
OCPP20InstallCertificateResponse,
+ OCPP20LogStatusNotificationResponse,
+ OCPP20NotifyCustomerInformationResponse,
OCPP20NotifyReportResponse,
OCPP20RequestStartTransactionResponse,
OCPP20RequestStopTransactionResponse,
OCPP20ResetResponse,
+ OCPP20SecurityEventNotificationResponse,
+ OCPP20SetNetworkProfileResponse,
OCPP20SetVariablesResponse,
OCPP20SignCertificateResponse,
OCPP20StatusNotificationResponse,
OCPP20TriggerMessageResponse,
OCPP20UnlockConnectorResponse,
+ OCPP20UpdateFirmwareResponse,
} from './ocpp/2.0/Responses.js'
export {
type ComponentType,
import type { JsonObject } from '../../JsonType.js'
import type { GenericStatus } from '../Common.js'
+export enum APNAuthenticationEnumType {
+ AUTO = 'AUTO',
+ CHAP = 'CHAP',
+ NONE = 'NONE',
+ PAP = 'PAP',
+}
+
export enum BootReasonEnumType {
ApplicationReset = 'ApplicationReset',
FirmwareUpdate = 'FirmwareUpdate',
V2GCertificate = 'V2GCertificate',
}
+export enum ChangeAvailabilityStatusEnumType {
+ Accepted = 'Accepted',
+ Rejected = 'Rejected',
+ Scheduled = 'Scheduled',
+}
+
+export enum CustomerInformationStatusEnumType {
+ Accepted = 'Accepted',
+ Invalid = 'Invalid',
+ Rejected = 'Rejected',
+}
+
export enum DataEnumType {
boolean = 'boolean',
dateTime = 'dateTime',
string = 'string',
}
+export enum DataTransferStatusEnumType {
+ Accepted = 'Accepted',
+ Rejected = 'Rejected',
+ UnknownMessageId = 'UnknownMessageId',
+ UnknownVendorId = 'UnknownVendorId',
+}
+
export enum DeleteCertificateStatusEnumType {
Accepted = 'Accepted',
Failed = 'Failed',
NotFound = 'NotFound',
}
+export enum FirmwareStatusEnumType {
+ Downloaded = 'Downloaded',
+ DownloadFailed = 'DownloadFailed',
+ Downloading = 'Downloading',
+ DownloadPaused = 'DownloadPaused',
+ DownloadScheduled = 'DownloadScheduled',
+ Idle = 'Idle',
+ InstallationFailed = 'InstallationFailed',
+ Installed = 'Installed',
+ Installing = 'Installing',
+ InstallRebooting = 'InstallRebooting',
+ InstallScheduled = 'InstallScheduled',
+ InstallVerificationFailed = 'InstallVerificationFailed',
+ InvalidSignature = 'InvalidSignature',
+ SignatureVerified = 'SignatureVerified',
+}
+
export enum GenericDeviceModelStatusEnumType {
Accepted = 'Accepted',
EmptyResultSet = 'EmptyResultSet',
Failed = 'Failed',
}
+export enum LogEnumType {
+ DiagnosticsLog = 'DiagnosticsLog',
+ SecurityLog = 'SecurityLog',
+}
+
+export enum LogStatusEnumType {
+ Accepted = 'Accepted',
+ AcceptedCanceled = 'AcceptedCanceled',
+ Rejected = 'Rejected',
+}
+
export enum MessageTriggerEnumType {
BootNotification = 'BootNotification',
FirmwareStatusNotification = 'FirmwareStatusNotification',
WATT_HOUR = 'Wh',
}
+export enum OCPPInterfaceEnumType {
+ Wired0 = 'Wired0',
+ Wired1 = 'Wired1',
+ Wired2 = 'Wired2',
+ Wired3 = 'Wired3',
+ Wireless0 = 'Wireless0',
+ Wireless1 = 'Wireless1',
+ Wireless2 = 'Wireless2',
+ Wireless3 = 'Wireless3',
+}
+
+export enum OCPPTransportEnumType {
+ JSON = 'JSON',
+ SOAP = 'SOAP',
+}
+
+export enum OCPPVersionEnumType {
+ OCPP12 = 'OCPP12',
+ OCPP15 = 'OCPP15',
+ OCPP16 = 'OCPP16',
+ OCPP20 = 'OCPP20',
+}
+
export enum OperationalStatusEnumType {
Inoperative = 'Inoperative',
Operative = 'Operative',
Scheduled = 'Scheduled',
}
+export enum SetNetworkProfileStatusEnumType {
+ Accepted = 'Accepted',
+ Failed = 'Failed',
+ Rejected = 'Rejected',
+}
+
export enum TriggerMessageStatusEnumType {
Accepted = 'Accepted',
NotImplemented = 'NotImplemented',
UnlockFailed = 'UnlockFailed',
}
+export enum UpdateFirmwareStatusEnumType {
+ Accepted = 'Accepted',
+ AcceptedCanceled = 'AcceptedCanceled',
+ InvalidCertificate = 'InvalidCertificate',
+ Rejected = 'Rejected',
+ RevokedCertificate = 'RevokedCertificate',
+}
+
+export enum UploadLogStatusEnumType {
+ AcceptedCanceled = 'AcceptedCanceled',
+ BadMessage = 'BadMessage',
+ Idle = 'Idle',
+ NotSupportedOperation = 'NotSupportedOperation',
+ PermissionDenied = 'PermissionDenied',
+ Uploaded = 'Uploaded',
+ UploadFailure = 'UploadFailure',
+ Uploading = 'Uploading',
+}
+
+export enum VPNEnumType {
+ IKEv2 = 'IKEv2',
+ IPSec = 'IPSec',
+ L2TP = 'L2TP',
+ PPTP = 'PPTP',
+}
+
+export interface APNType extends JsonObject {
+ apn: string
+ apnAuthentication: APNAuthenticationEnumType
+ apnPassword?: string
+ apnUserName?: string
+ customData?: CustomDataType
+ preferredNetwork?: string
+ simPin?: number
+ useOnlyPreferredNetwork?: boolean
+}
+
export interface CertificateHashDataChainType extends JsonObject {
certificateHashData: CertificateHashDataType
certificateType: GetCertificateIdUseEnumType
vendorId: string
}
+export interface FirmwareType extends JsonObject {
+ customData?: CustomDataType
+ installDateTime?: Date
+ location: string
+ retrieveDateTime: Date
+ signature?: string
+ signingCertificate?: string
+}
+
export type GenericStatusEnumType = GenericStatus
+export interface LogParametersType extends JsonObject {
+ customData?: CustomDataType
+ latestTimestamp?: Date
+ oldestTimestamp?: Date
+ remoteLocation: string
+}
+
export interface ModemType extends JsonObject {
customData?: CustomDataType
iccid?: string
imsi?: string
}
+export interface NetworkConnectionProfileType extends JsonObject {
+ apn?: APNType
+ customData?: CustomDataType
+ messageTimeout: number
+ ocppCsmsUrl: string
+ ocppInterface: OCPPInterfaceEnumType
+ ocppTransport: OCPPTransportEnumType
+ ocppVersion: OCPPVersionEnumType
+ securityProfile: number
+ vpn?: VPNType
+}
+
export interface OCSPRequestDataType extends JsonObject {
hashAlgorithm: HashAlgorithmEnumType
issuerKeyHash: string
customData?: CustomDataType
reasonCode: ReasonCodeEnumType
}
+
+export interface VPNType extends JsonObject {
+ customData?: CustomDataType
+ group?: string
+ key: string
+ password: string
+ server: string
+ type: VPNEnumType
+ user: string
+}
CertificateSigningUseEnumType,
ChargingStationType,
CustomDataType,
+ FirmwareStatusEnumType,
+ FirmwareType,
GetCertificateIdUseEnumType,
InstallCertificateUseEnumType,
+ LogEnumType,
+ LogParametersType,
MessageTriggerEnumType,
+ NetworkConnectionProfileType,
OCSPRequestDataType,
+ OperationalStatusEnumType,
ReportBaseEnumType,
ResetEnumType,
+ UploadLogStatusEnumType,
} from './Common.js'
import type {
OCPP20ChargingProfileType,
export enum OCPP20IncomingRequestCommand {
CERTIFICATE_SIGNED = 'CertificateSigned',
+ CHANGE_AVAILABILITY = 'ChangeAvailability',
CLEAR_CACHE = 'ClearCache',
+ CUSTOMER_INFORMATION = 'CustomerInformation',
+ DATA_TRANSFER = 'DataTransfer',
DELETE_CERTIFICATE = 'DeleteCertificate',
GET_BASE_REPORT = 'GetBaseReport',
GET_INSTALLED_CERTIFICATE_IDS = 'GetInstalledCertificateIds',
+ GET_LOG = 'GetLog',
+ GET_TRANSACTION_STATUS = 'GetTransactionStatus',
GET_VARIABLES = 'GetVariables',
INSTALL_CERTIFICATE = 'InstallCertificate',
REQUEST_START_TRANSACTION = 'RequestStartTransaction',
REQUEST_STOP_TRANSACTION = 'RequestStopTransaction',
RESET = 'Reset',
+ SET_NETWORK_PROFILE = 'SetNetworkProfile',
SET_VARIABLES = 'SetVariables',
TRIGGER_MESSAGE = 'TriggerMessage',
UNLOCK_CONNECTOR = 'UnlockConnector',
+ UPDATE_FIRMWARE = 'UpdateFirmware',
}
export enum OCPP20RequestCommand {
BOOT_NOTIFICATION = 'BootNotification',
+ FIRMWARE_STATUS_NOTIFICATION = 'FirmwareStatusNotification',
GET_15118_EV_CERTIFICATE = 'Get15118EVCertificate',
GET_CERTIFICATE_STATUS = 'GetCertificateStatus',
HEARTBEAT = 'Heartbeat',
+ LOG_STATUS_NOTIFICATION = 'LogStatusNotification',
+ METER_VALUES = 'MeterValues',
+ NOTIFY_CUSTOMER_INFORMATION = 'NotifyCustomerInformation',
NOTIFY_REPORT = 'NotifyReport',
+ SECURITY_EVENT_NOTIFICATION = 'SecurityEventNotification',
SIGN_CERTIFICATE = 'SignCertificate',
STATUS_NOTIFICATION = 'StatusNotification',
TRANSACTION_EVENT = 'TransactionEvent',
customData?: CustomDataType
}
+export interface OCPP20ChangeAvailabilityRequest extends JsonObject {
+ customData?: CustomDataType
+ evse?: OCPP20EVSEType
+ operationalStatus: OperationalStatusEnumType
+}
+
export type OCPP20ClearCacheRequest = EmptyObject
+export interface OCPP20CustomerInformationRequest extends JsonObject {
+ clear: boolean
+ customData?: CustomDataType
+ customerCertificate?: CertificateHashDataType
+ customerIdentifier?: string
+ idToken?: OCPP20IdTokenType
+ report: boolean
+ requestId: number
+}
+
+export interface OCPP20DataTransferRequest extends JsonObject {
+ customData?: CustomDataType
+ data?: JsonObject
+ messageId?: string
+ vendorId: string
+}
+
export interface OCPP20DeleteCertificateRequest extends JsonObject {
certificateHashData: CertificateHashDataType
customData?: CustomDataType
}
+export interface OCPP20FirmwareStatusNotificationRequest extends JsonObject {
+ customData?: CustomDataType
+ requestId?: number
+ status: FirmwareStatusEnumType
+}
+
export interface OCPP20Get15118EVCertificateRequest extends JsonObject {
action: CertificateActionEnumType
customData?: CustomDataType
customData?: CustomDataType
}
+export interface OCPP20GetLogRequest extends JsonObject {
+ customData?: CustomDataType
+ log: LogParametersType
+ logType: LogEnumType
+ requestId: number
+ retries?: number
+ retryInterval?: number
+}
+
+export interface OCPP20GetTransactionStatusRequest extends JsonObject {
+ customData?: CustomDataType
+ transactionId?: string
+}
+
export interface OCPP20GetVariablesRequest extends JsonObject {
customData?: CustomDataType
getVariableData: OCPP20GetVariableDataType[]
customData?: CustomDataType
}
+export interface OCPP20LogStatusNotificationRequest extends JsonObject {
+ customData?: CustomDataType
+ requestId?: number
+ status: UploadLogStatusEnumType
+}
+
+export interface OCPP20NotifyCustomerInformationRequest extends JsonObject {
+ customData?: CustomDataType
+ data: string
+ generatedAt: Date
+ requestId: number
+ seqNo: number
+ tbc?: boolean
+}
+
export interface OCPP20NotifyReportRequest extends JsonObject {
customData?: CustomDataType
generatedAt: Date
type: ResetEnumType
}
+export interface OCPP20SecurityEventNotificationRequest extends JsonObject {
+ customData?: CustomDataType
+ techInfo?: string
+ timestamp: Date
+ type: string
+}
+
+export interface OCPP20SetNetworkProfileRequest extends JsonObject {
+ configurationSlot: number
+ connectionData: NetworkConnectionProfileType
+ customData?: CustomDataType
+}
+
export interface OCPP20SetVariablesRequest extends JsonObject {
customData?: CustomDataType
setVariableData: OCPP20SetVariableDataType[]
customData?: CustomDataType
evseId: number
}
+
+export interface OCPP20UpdateFirmwareRequest extends JsonObject {
+ customData?: CustomDataType
+ firmware: FirmwareType
+ requestId: number
+ retries?: number
+ retryInterval?: number
+}
import type {
CertificateHashDataChainType,
CertificateSignedStatusEnumType,
+ ChangeAvailabilityStatusEnumType,
CustomDataType,
+ CustomerInformationStatusEnumType,
+ DataTransferStatusEnumType,
DeleteCertificateStatusEnumType,
GenericDeviceModelStatusEnumType,
GenericStatusEnumType,
GetInstalledCertificateStatusEnumType,
InstallCertificateStatusEnumType,
Iso15118EVCertificateStatusEnumType,
+ LogStatusEnumType,
ResetStatusEnumType,
+ SetNetworkProfileStatusEnumType,
StatusInfoType,
TriggerMessageStatusEnumType,
UnlockStatusEnumType,
+ UpdateFirmwareStatusEnumType,
} from './Common.js'
import type { RequestStartStopStatusEnumType } from './Transaction.js'
import type { OCPP20GetVariableResultType, OCPP20SetVariableResultType } from './Variables.js'
statusInfo?: StatusInfoType
}
+export interface OCPP20ChangeAvailabilityResponse extends JsonObject {
+ customData?: CustomDataType
+ status: ChangeAvailabilityStatusEnumType
+ statusInfo?: StatusInfoType
+}
+
export interface OCPP20ClearCacheResponse extends JsonObject {
customData?: CustomDataType
status: GenericStatusEnumType
statusInfo?: StatusInfoType
}
+export interface OCPP20CustomerInformationResponse extends JsonObject {
+ customData?: CustomDataType
+ status: CustomerInformationStatusEnumType
+ statusInfo?: StatusInfoType
+}
+
+export interface OCPP20DataTransferResponse extends JsonObject {
+ customData?: CustomDataType
+ data?: JsonObject
+ status: DataTransferStatusEnumType
+ statusInfo?: StatusInfoType
+}
+
export interface OCPP20DeleteCertificateResponse extends JsonObject {
customData?: CustomDataType
status: DeleteCertificateStatusEnumType
statusInfo?: StatusInfoType
}
+export type OCPP20FirmwareStatusNotificationResponse = EmptyObject
+
export interface OCPP20Get15118EVCertificateResponse extends JsonObject {
customData?: CustomDataType
exiResponse: string
statusInfo?: StatusInfoType
}
+export interface OCPP20GetLogResponse extends JsonObject {
+ customData?: CustomDataType
+ filename?: string
+ status: LogStatusEnumType
+ statusInfo?: StatusInfoType
+}
+
+export interface OCPP20GetTransactionStatusResponse extends JsonObject {
+ customData?: CustomDataType
+ messagesInQueue: boolean
+ ongoingIndicator?: boolean
+}
+
export interface OCPP20GetVariablesResponse extends JsonObject {
customData?: CustomDataType
getVariableResult: OCPP20GetVariableResultType[]
statusInfo?: StatusInfoType
}
+export type OCPP20LogStatusNotificationResponse = EmptyObject
+
+export type OCPP20NotifyCustomerInformationResponse = EmptyObject
+
export type OCPP20NotifyReportResponse = EmptyObject
export interface OCPP20RequestStartTransactionResponse extends JsonObject {
statusInfo?: StatusInfoType
}
+export type OCPP20SecurityEventNotificationResponse = EmptyObject
+
+export interface OCPP20SetNetworkProfileResponse extends JsonObject {
+ customData?: CustomDataType
+ status: SetNetworkProfileStatusEnumType
+ statusInfo?: StatusInfoType
+}
+
export interface OCPP20SetVariablesResponse extends JsonObject {
customData?: CustomDataType
setVariableResult: OCPP20SetVariableResultType[]
status: UnlockStatusEnumType
statusInfo?: StatusInfoType
}
+
+export interface OCPP20UpdateFirmwareResponse extends JsonObject {
+ customData?: CustomDataType
+ status: UpdateFirmwareStatusEnumType
+ statusInfo?: StatusInfoType
+}
--- /dev/null
+/**
+ * @file Tests for OCPP20IncomingRequestService ChangeAvailability
+ * @description Unit tests for OCPP 2.0.1 ChangeAvailability command handling (G03)
+ */
+
+import assert from 'node:assert/strict'
+import { afterEach, beforeEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../../../src/charging-station/index.js'
+
+import { createTestableIncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
+import { OCPP20IncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.js'
+import {
+ ChangeAvailabilityStatusEnumType,
+ OCPPVersion,
+ OperationalStatusEnumType,
+} 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 { createMockChargingStation } from '../../ChargingStationTestUtils.js'
+
+await describe('G03 - ChangeAvailability', async () => {
+ let station: ChargingStation
+ let testableService: ReturnType<typeof createTestableIncomingRequestService>
+
+ beforeEach(() => {
+ const { station: mockStation } = createMockChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ ocppRequestService: {
+ requestHandler: async () => await Promise.resolve({}),
+ },
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ station = mockStation
+ const incomingRequestService = new OCPP20IncomingRequestService()
+ testableService = createTestableIncomingRequestService(incomingRequestService)
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ // FR: G03.FR.01
+ await it('should accept EVSE-level Inoperative when no ongoing transaction', () => {
+ const response = testableService.handleRequestChangeAvailability(station, {
+ evse: { id: 1 },
+ operationalStatus: OperationalStatusEnumType.Inoperative,
+ })
+
+ assert.strictEqual(response.status, ChangeAvailabilityStatusEnumType.Accepted)
+ const evseStatus = station.getEvseStatus(1)
+ assert.strictEqual(evseStatus?.availability, OperationalStatusEnumType.Inoperative)
+ })
+
+ // FR: G03.FR.02
+ await it('should accept CS-level Inoperative when no ongoing transaction', () => {
+ const response = testableService.handleRequestChangeAvailability(station, {
+ operationalStatus: OperationalStatusEnumType.Inoperative,
+ })
+
+ assert.strictEqual(response.status, ChangeAvailabilityStatusEnumType.Accepted)
+ for (const [evseId, evseStatus] of station.evses) {
+ if (evseId > 0) {
+ assert.strictEqual(
+ evseStatus.availability,
+ OperationalStatusEnumType.Inoperative,
+ `EVSE ${String(evseId)} should be Inoperative`
+ )
+ }
+ }
+ })
+
+ // FR: G03.FR.03
+ await it('should schedule EVSE-level Inoperative when ongoing transaction exists', () => {
+ setupConnectorWithTransaction(station, 1, {
+ transactionId: 100,
+ })
+
+ const response = testableService.handleRequestChangeAvailability(station, {
+ evse: { id: 1 },
+ operationalStatus: OperationalStatusEnumType.Inoperative,
+ })
+
+ assert.strictEqual(response.status, ChangeAvailabilityStatusEnumType.Scheduled)
+ })
+
+ // FR: G03.FR.04
+ await it('should schedule CS-level Inoperative when some EVSEs have transactions', () => {
+ setupConnectorWithTransaction(station, 2, {
+ transactionId: 200,
+ })
+
+ const response = testableService.handleRequestChangeAvailability(station, {
+ operationalStatus: OperationalStatusEnumType.Inoperative,
+ })
+
+ assert.strictEqual(response.status, ChangeAvailabilityStatusEnumType.Scheduled)
+ })
+
+ await it('should reject when EVSE does not exist', () => {
+ const response = testableService.handleRequestChangeAvailability(station, {
+ evse: { id: 999 },
+ operationalStatus: OperationalStatusEnumType.Inoperative,
+ })
+
+ assert.strictEqual(response.status, ChangeAvailabilityStatusEnumType.Rejected)
+ assert.notStrictEqual(response.statusInfo, undefined)
+ assert.strictEqual(response.statusInfo?.reasonCode, 'UnknownEvse')
+ })
+
+ await it('should accept when already in requested state (idempotent)', () => {
+ const evseStatus = station.getEvseStatus(1)
+ if (evseStatus != null) {
+ evseStatus.availability = OperationalStatusEnumType.Operative
+ }
+
+ const response = testableService.handleRequestChangeAvailability(station, {
+ evse: { id: 1 },
+ operationalStatus: OperationalStatusEnumType.Operative,
+ })
+
+ assert.strictEqual(response.status, ChangeAvailabilityStatusEnumType.Accepted)
+ assert.strictEqual(evseStatus?.availability, OperationalStatusEnumType.Operative)
+ })
+
+ await it('should set Operative after Inoperative, connectors return to Available', () => {
+ const evseStatus = station.getEvseStatus(1)
+ if (evseStatus != null) {
+ evseStatus.availability = OperationalStatusEnumType.Inoperative
+ }
+
+ const response = testableService.handleRequestChangeAvailability(station, {
+ evse: { id: 1 },
+ operationalStatus: OperationalStatusEnumType.Operative,
+ })
+
+ assert.strictEqual(response.status, ChangeAvailabilityStatusEnumType.Accepted)
+ assert.strictEqual(evseStatus?.availability, OperationalStatusEnumType.Operative)
+ })
+
+ await it('should accept CS-level change with evse.id === 0', () => {
+ const response = testableService.handleRequestChangeAvailability(station, {
+ evse: { id: 0 },
+ operationalStatus: OperationalStatusEnumType.Inoperative,
+ })
+
+ assert.strictEqual(response.status, ChangeAvailabilityStatusEnumType.Accepted)
+ for (const [evseId, evseStatus] of station.evses) {
+ if (evseId > 0) {
+ assert.strictEqual(evseStatus.availability, OperationalStatusEnumType.Inoperative)
+ }
+ }
+ })
+})
--- /dev/null
+/**
+ * @file Tests for OCPP20IncomingRequestService CustomerInformation
+ * @description Unit tests for OCPP 2.0.1 CustomerInformation command handling
+ */
+
+import assert from 'node:assert/strict'
+import { afterEach, beforeEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../../../src/charging-station/index.js'
+
+import { createTestableIncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
+import { OCPP20IncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.js'
+import { CustomerInformationStatusEnumType, OCPPVersion } from '../../../../src/types/index.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
+import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
+
+await describe('N32 - CustomerInformation', async () => {
+ let station: ChargingStation
+ let testableService: ReturnType<typeof createTestableIncomingRequestService>
+
+ beforeEach(() => {
+ const { station: mockStation } = createMockChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ station = mockStation
+ testableService = createTestableIncomingRequestService(new OCPP20IncomingRequestService())
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ // TC_N_32_CS: CS must respond to CustomerInformation with Accepted for clear requests
+ await it('should respond with Accepted status for clear request', () => {
+ const response = testableService.handleRequestCustomerInformation(station, {
+ clear: true,
+ report: false,
+ requestId: 1,
+ })
+
+ assert.notStrictEqual(response, undefined)
+ assert.strictEqual(typeof response, 'object')
+ assert.notStrictEqual(response.status, undefined)
+ assert.strictEqual(response.status, CustomerInformationStatusEnumType.Accepted)
+ })
+
+ // TC_N_32_CS: CS must respond to CustomerInformation with Accepted for report requests
+ await it('should respond with Accepted status for report request', () => {
+ const response = testableService.handleRequestCustomerInformation(station, {
+ clear: false,
+ report: true,
+ requestId: 2,
+ })
+
+ assert.notStrictEqual(response, undefined)
+ assert.strictEqual(typeof response, 'object')
+ assert.notStrictEqual(response.status, undefined)
+ assert.strictEqual(response.status, CustomerInformationStatusEnumType.Accepted)
+ })
+
+ // TC_N_32_CS: CS must respond with Rejected when neither clear nor report is set
+ await it('should respond with Rejected status when neither clear nor report is set', () => {
+ const response = testableService.handleRequestCustomerInformation(station, {
+ clear: false,
+ report: false,
+ requestId: 3,
+ })
+
+ assert.notStrictEqual(response, undefined)
+ assert.strictEqual(typeof response, 'object')
+ assert.notStrictEqual(response.status, undefined)
+ assert.strictEqual(response.status, CustomerInformationStatusEnumType.Rejected)
+ })
+
+ // Verify clear request with explicit false report flag
+ await it('should respond with Accepted for clear=true and report=false', () => {
+ const response = testableService.handleRequestCustomerInformation(station, {
+ clear: true,
+ report: false,
+ requestId: 4,
+ })
+
+ assert.strictEqual(response.status, CustomerInformationStatusEnumType.Accepted)
+ })
+
+ // Verify report request with explicit false clear flag
+ await it('should respond with Accepted for clear=false and report=true', () => {
+ const response = testableService.handleRequestCustomerInformation(station, {
+ clear: false,
+ report: true,
+ requestId: 5,
+ })
+
+ assert.strictEqual(response.status, CustomerInformationStatusEnumType.Accepted)
+ })
+})
--- /dev/null
+/**
+ * @file Tests for OCPP20IncomingRequestService DataTransfer
+ * @description Unit tests for OCPP 2.0 DataTransfer command handling
+ */
+
+import assert from 'node:assert/strict'
+import { afterEach, beforeEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../../../src/charging-station/index.js'
+
+import { createTestableIncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
+import { OCPP20IncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.js'
+import { DataTransferStatusEnumType, OCPPVersion } from '../../../../src/types/index.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
+import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
+
+await describe('P01 - DataTransfer', async () => {
+ let station: ChargingStation
+ let testableService: ReturnType<typeof createTestableIncomingRequestService>
+
+ beforeEach(() => {
+ const { station: mockStation } = createMockChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ station = mockStation
+ testableService = createTestableIncomingRequestService(new OCPP20IncomingRequestService())
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ // TC_P_01_CS: CS with no vendor extensions must respond UnknownVendorId
+ await it('should respond with UnknownVendorId status', () => {
+ const response = testableService.handleRequestDataTransfer(station, {
+ vendorId: 'TestVendor',
+ })
+
+ assert.notStrictEqual(response, undefined)
+ assert.strictEqual(typeof response, 'object')
+ assert.notStrictEqual(response.status, undefined)
+ assert.strictEqual(response.status, DataTransferStatusEnumType.UnknownVendorId)
+ })
+
+ // TC_P_01_CS: Verify response is UnknownVendorId regardless of vendorId value
+ await it('should respond with UnknownVendorId regardless of vendorId', () => {
+ const response = testableService.handleRequestDataTransfer(station, {
+ vendorId: 'AnotherVendor',
+ })
+
+ assert.notStrictEqual(response, undefined)
+ assert.strictEqual(response.status, DataTransferStatusEnumType.UnknownVendorId)
+ })
+})
--- /dev/null
+/**
+ * @file Tests for OCPP20IncomingRequestService GetLog
+ * @description Unit tests for OCPP 2.0.1 GetLog command handling (K01)
+ */
+
+import assert from 'node:assert/strict'
+import { afterEach, beforeEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../../../src/charging-station/index.js'
+
+import { createTestableIncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
+import { OCPP20IncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.js'
+import {
+ LogEnumType,
+ LogStatusEnumType,
+ type OCPP20GetLogRequest,
+ OCPPVersion,
+} from '../../../../src/types/index.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
+import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
+
+await describe('K01 - GetLog', async () => {
+ let station: ChargingStation
+ let testableService: ReturnType<typeof createTestableIncomingRequestService>
+
+ beforeEach(() => {
+ const { station: mockStation } = createMockChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 1,
+ evseConfiguration: { evsesCount: 1 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ station = mockStation
+ testableService = createTestableIncomingRequestService(new OCPP20IncomingRequestService())
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ await it('should return Accepted with filename for DiagnosticsLog request', () => {
+ const request: OCPP20GetLogRequest = {
+ log: {
+ remoteLocation: 'ftp://logs.example.com/uploads/',
+ },
+ logType: LogEnumType.DiagnosticsLog,
+ requestId: 1,
+ }
+
+ const response = testableService.handleRequestGetLog(station, request)
+
+ assert.notStrictEqual(response, undefined)
+ assert.strictEqual(typeof response, 'object')
+ assert.strictEqual(response.status, LogStatusEnumType.Accepted)
+ assert.strictEqual(response.filename, 'simulator-log.txt')
+ })
+
+ await it('should return Accepted with filename for SecurityLog request', () => {
+ const request: OCPP20GetLogRequest = {
+ log: {
+ remoteLocation: 'https://logs.example.com/security/',
+ },
+ logType: LogEnumType.SecurityLog,
+ requestId: 2,
+ }
+
+ const response = testableService.handleRequestGetLog(station, request)
+
+ assert.strictEqual(response.status, LogStatusEnumType.Accepted)
+ assert.strictEqual(response.filename, 'simulator-log.txt')
+ })
+
+ await it('should pass through requestId correctly across different values', () => {
+ const testRequestId = 42
+ const request: OCPP20GetLogRequest = {
+ log: {
+ remoteLocation: 'ftp://logs.example.com/uploads/',
+ },
+ logType: LogEnumType.DiagnosticsLog,
+ requestId: testRequestId,
+ }
+
+ const response = testableService.handleRequestGetLog(station, request)
+
+ assert.strictEqual(response.status, LogStatusEnumType.Accepted)
+ assert.strictEqual(typeof response.status, 'string')
+ assert.strictEqual(response.filename, 'simulator-log.txt')
+ })
+
+ await it('should return Accepted for request with retries and retryInterval', () => {
+ const request: OCPP20GetLogRequest = {
+ log: {
+ latestTimestamp: new Date('2025-01-15T23:59:59.000Z'),
+ oldestTimestamp: new Date('2025-01-01T00:00:00.000Z'),
+ remoteLocation: 'ftp://logs.example.com/uploads/',
+ },
+ logType: LogEnumType.DiagnosticsLog,
+ requestId: 5,
+ retries: 3,
+ retryInterval: 60,
+ }
+
+ const response = testableService.handleRequestGetLog(station, request)
+
+ assert.strictEqual(response.status, LogStatusEnumType.Accepted)
+ assert.strictEqual(response.filename, 'simulator-log.txt')
+ })
+})
--- /dev/null
+/**
+ * @file Tests for OCPP20IncomingRequestService GetTransactionStatus
+ * @description Unit tests for OCPP 2.0.1 GetTransactionStatus command handling
+ */
+
+import assert from 'node:assert/strict'
+import { afterEach, beforeEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../../../src/charging-station/index.js'
+
+import { createTestableIncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
+import { OCPP20IncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.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 { createMockChargingStation } from '../../ChargingStationTestUtils.js'
+
+await describe('D14 - GetTransactionStatus', async () => {
+ let station: ChargingStation
+ let testableService: ReturnType<typeof createTestableIncomingRequestService>
+
+ beforeEach(() => {
+ const { station: mockStation } = createMockChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ station = mockStation
+ testableService = createTestableIncomingRequestService(new OCPP20IncomingRequestService())
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ // E14.FR.06: When no transactionId provided, ongoingIndicator SHALL NOT be set
+ await it('should not include ongoingIndicator when no transactionId provided (E14.FR.06)', () => {
+ const response = testableService.handleRequestGetTransactionStatus(station, {})
+
+ assert.notStrictEqual(response, undefined)
+ assert.strictEqual(typeof response, 'object')
+ assert.strictEqual(response.ongoingIndicator, undefined)
+ assert.strictEqual(response.messagesInQueue, false)
+ })
+
+ // E14.FR.06: Even with active transactions, no transactionId → ongoingIndicator not set
+ await it('should not include ongoingIndicator when active transaction exists but no transactionId (E14.FR.06)', () => {
+ const transactionId = 'txn-12345'
+ setupConnectorWithTransaction(station, 1, {
+ transactionId: transactionId as unknown as number,
+ })
+
+ const response = testableService.handleRequestGetTransactionStatus(station, {})
+
+ assert.notStrictEqual(response, undefined)
+ assert.strictEqual(response.ongoingIndicator, undefined)
+ assert.strictEqual(response.messagesInQueue, false)
+ })
+
+ // E14.FR.01: Unknown transactionId → ongoingIndicator: false
+ await it('should return ongoingIndicator false when specific transactionId does not exist', () => {
+ const response = testableService.handleRequestGetTransactionStatus(station, {
+ transactionId: 'nonexistent-txn-id',
+ })
+
+ assert.notStrictEqual(response, undefined)
+ assert.strictEqual(response.ongoingIndicator, false)
+ assert.strictEqual(response.messagesInQueue, false)
+ })
+
+ // E14.FR.02: Active transaction with transactionId → ongoingIndicator: true
+ await it('should return ongoingIndicator true when specific transactionId exists', () => {
+ const transactionId = 'txn-67890'
+ setupConnectorWithTransaction(station, 2, {
+ transactionId: transactionId as unknown as number,
+ })
+
+ const response = testableService.handleRequestGetTransactionStatus(station, {
+ transactionId,
+ })
+
+ assert.notStrictEqual(response, undefined)
+ assert.strictEqual(response.ongoingIndicator, true)
+ assert.strictEqual(response.messagesInQueue, false)
+ })
+})
--- /dev/null
+/**
+ * @file Tests for OCPP20IncomingRequestService SetNetworkProfile
+ * @description Unit tests for OCPP 2.0.1 SetNetworkProfile command handling
+ */
+
+import assert from 'node:assert/strict'
+import { afterEach, beforeEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../../../src/charging-station/index.js'
+
+import { createTestableIncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
+import { OCPP20IncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.js'
+import {
+ OCPPVersion,
+ ReasonCodeEnumType,
+ SetNetworkProfileStatusEnumType,
+} from '../../../../src/types/index.js'
+import {
+ OCPPInterfaceEnumType,
+ OCPPTransportEnumType,
+ OCPPVersionEnumType,
+} from '../../../../src/types/ocpp/2.0/Common.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
+import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
+
+await describe('B43 - SetNetworkProfile', async () => {
+ let station: ChargingStation
+ let testableService: ReturnType<typeof createTestableIncomingRequestService>
+
+ beforeEach(() => {
+ const { station: mockStation } = createMockChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ station = mockStation
+ testableService = createTestableIncomingRequestService(new OCPP20IncomingRequestService())
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ // TC_B_43_CS: CS must respond to SetNetworkProfile at minimum with Rejected
+ await it('should respond with Rejected status', () => {
+ const response = testableService.handleRequestSetNetworkProfile(station, {
+ configurationSlot: 1,
+ connectionData: {
+ messageTimeout: 30,
+ ocppCsmsUrl: 'wss://example.com/ocpp',
+ ocppInterface: OCPPInterfaceEnumType.Wired0,
+ ocppTransport: OCPPTransportEnumType.JSON,
+ ocppVersion: OCPPVersionEnumType.OCPP20,
+ securityProfile: 3,
+ },
+ })
+
+ assert.notStrictEqual(response, undefined)
+ assert.strictEqual(typeof response, 'object')
+ assert.notStrictEqual(response.status, undefined)
+ assert.strictEqual(response.status, SetNetworkProfileStatusEnumType.Rejected)
+ })
+
+ // TC_B_43_CS: Verify response includes statusInfo with reasonCode
+ await it('should include statusInfo with UnsupportedRequest reasonCode', () => {
+ const response = testableService.handleRequestSetNetworkProfile(station, {
+ configurationSlot: 1,
+ connectionData: {
+ messageTimeout: 30,
+ ocppCsmsUrl: 'wss://example.com/ocpp',
+ ocppInterface: OCPPInterfaceEnumType.Wired0,
+ ocppTransport: OCPPTransportEnumType.JSON,
+ ocppVersion: OCPPVersionEnumType.OCPP20,
+ securityProfile: 3,
+ },
+ })
+
+ assert.notStrictEqual(response, undefined)
+ assert.strictEqual(response.status, SetNetworkProfileStatusEnumType.Rejected)
+ assert.notStrictEqual(response.statusInfo, undefined)
+ assert.strictEqual(response.statusInfo?.reasonCode, ReasonCodeEnumType.UnsupportedRequest)
+ })
+})
assert.strictEqual(response.status, TriggerMessageStatusEnumType.Accepted)
})
- })
- await describe('F06 - NotImplemented triggers', async () => {
- let mockStation: MockChargingStation
+ await it('should return Accepted for MeterValues trigger', () => {
+ const request: OCPP20TriggerMessageRequest = {
+ requestedMessage: MessageTriggerEnumType.MeterValues,
+ }
- beforeEach(() => {
- ;({ mockStation } = createTriggerMessageStation())
+ const response: OCPP20TriggerMessageResponse = testableService.handleRequestTriggerMessage(
+ mockStation,
+ request
+ )
+
+ assert.strictEqual(response.status, TriggerMessageStatusEnumType.Accepted)
+ assert.strictEqual(response.statusInfo, undefined)
})
- await it('should return NotImplemented for MeterValues trigger', () => {
+ await it('should return Accepted for MeterValues trigger with specific EVSE', () => {
const request: OCPP20TriggerMessageRequest = {
+ evse: { id: 1 },
requestedMessage: MessageTriggerEnumType.MeterValues,
}
request
)
- assert.strictEqual(response.status, TriggerMessageStatusEnumType.NotImplemented)
- if (response.statusInfo == null) {
- assert.fail('Expected statusInfo to be defined')
+ assert.strictEqual(response.status, TriggerMessageStatusEnumType.Accepted)
+ assert.strictEqual(response.statusInfo, undefined)
+ })
+
+ await it('should return Accepted for FirmwareStatusNotification trigger', () => {
+ const request: OCPP20TriggerMessageRequest = {
+ requestedMessage: MessageTriggerEnumType.FirmwareStatusNotification,
}
- assert.strictEqual(response.statusInfo.reasonCode, ReasonCodeEnumType.UnsupportedRequest)
- if (response.statusInfo.additionalInfo == null) {
- assert.fail('Expected additionalInfo to be defined')
+
+ const response: OCPP20TriggerMessageResponse = testableService.handleRequestTriggerMessage(
+ mockStation,
+ request
+ )
+
+ assert.strictEqual(response.status, TriggerMessageStatusEnumType.Accepted)
+ assert.strictEqual(response.statusInfo, undefined)
+ })
+
+ await it('should return Accepted for LogStatusNotification trigger', () => {
+ const request: OCPP20TriggerMessageRequest = {
+ requestedMessage: MessageTriggerEnumType.LogStatusNotification,
}
- assert.ok(response.statusInfo.additionalInfo.includes('MeterValues'))
+
+ const response: OCPP20TriggerMessageResponse = testableService.handleRequestTriggerMessage(
+ mockStation,
+ request
+ )
+
+ assert.strictEqual(response.status, TriggerMessageStatusEnumType.Accepted)
+ assert.strictEqual(response.statusInfo, undefined)
+ })
+ })
+
+ await describe('F06 - NotImplemented triggers', async () => {
+ let mockStation: MockChargingStation
+
+ beforeEach(() => {
+ ;({ mockStation } = createTriggerMessageStation())
})
await it('should return NotImplemented for TransactionEvent trigger', () => {
assert.strictEqual(response.statusInfo?.reasonCode, ReasonCodeEnumType.UnsupportedRequest)
})
- await it('should return NotImplemented for LogStatusNotification trigger', () => {
+ await it('should return NotImplemented for PublishFirmwareStatusNotification trigger', () => {
const request: OCPP20TriggerMessageRequest = {
- requestedMessage: MessageTriggerEnumType.LogStatusNotification,
+ requestedMessage: MessageTriggerEnumType.PublishFirmwareStatusNotification,
}
const response: OCPP20TriggerMessageResponse = testableService.handleRequestTriggerMessage(
assert.strictEqual(response.statusInfo?.reasonCode, ReasonCodeEnumType.UnsupportedRequest)
})
- await it('should return NotImplemented for FirmwareStatusNotification trigger', () => {
+ await it('should return NotImplemented for SignChargingStationCertificate trigger', () => {
const request: OCPP20TriggerMessageRequest = {
- requestedMessage: MessageTriggerEnumType.FirmwareStatusNotification,
+ requestedMessage: MessageTriggerEnumType.SignChargingStationCertificate,
}
const response: OCPP20TriggerMessageResponse = testableService.handleRequestTriggerMessage(
--- /dev/null
+/**
+ * @file Tests for OCPP20IncomingRequestService UpdateFirmware
+ * @description Unit tests for OCPP 2.0.1 UpdateFirmware command handling (J02)
+ */
+
+import assert from 'node:assert/strict'
+import { afterEach, beforeEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../../../src/charging-station/index.js'
+
+import { createTestableIncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
+import { OCPP20IncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.js'
+import {
+ type OCPP20UpdateFirmwareRequest,
+ OCPPVersion,
+ UpdateFirmwareStatusEnumType,
+} from '../../../../src/types/index.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
+import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
+
+await describe('J02 - UpdateFirmware', async () => {
+ let station: ChargingStation
+ let testableService: ReturnType<typeof createTestableIncomingRequestService>
+
+ beforeEach(() => {
+ const { station: mockStation } = createMockChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 1,
+ evseConfiguration: { evsesCount: 1 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ station = mockStation
+ testableService = createTestableIncomingRequestService(new OCPP20IncomingRequestService())
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ await it('should return Accepted for valid firmware update request', () => {
+ const request: OCPP20UpdateFirmwareRequest = {
+ firmware: {
+ location: 'https://firmware.example.com/update-v2.0.bin',
+ retrieveDateTime: new Date('2025-01-15T10:00:00.000Z'),
+ },
+ requestId: 1,
+ }
+
+ const response = testableService.handleRequestUpdateFirmware(station, request)
+
+ assert.notStrictEqual(response, undefined)
+ assert.strictEqual(typeof response, 'object')
+ assert.strictEqual(response.status, UpdateFirmwareStatusEnumType.Accepted)
+ })
+
+ await it('should return Accepted for request with signature field', () => {
+ const request: OCPP20UpdateFirmwareRequest = {
+ firmware: {
+ location: 'https://firmware.example.com/signed-update.bin',
+ retrieveDateTime: new Date('2025-01-15T10:00:00.000Z'),
+ signature: 'dGVzdC1zaWduYXR1cmU=',
+ signingCertificate: '-----BEGIN CERTIFICATE-----\nMIIBkTCB...',
+ },
+ requestId: 2,
+ }
+
+ const response = testableService.handleRequestUpdateFirmware(station, request)
+
+ assert.strictEqual(response.status, UpdateFirmwareStatusEnumType.Accepted)
+ })
+
+ await it('should return Accepted for request without signature', () => {
+ const request: OCPP20UpdateFirmwareRequest = {
+ firmware: {
+ location: 'https://firmware.example.com/unsigned-update.bin',
+ retrieveDateTime: new Date('2025-01-15T12:00:00.000Z'),
+ },
+ requestId: 3,
+ }
+
+ const response = testableService.handleRequestUpdateFirmware(station, request)
+
+ assert.strictEqual(response.status, UpdateFirmwareStatusEnumType.Accepted)
+ })
+
+ await it('should pass through requestId correctly in the response', () => {
+ const testRequestId = 42
+ const request: OCPP20UpdateFirmwareRequest = {
+ firmware: {
+ location: 'https://firmware.example.com/update.bin',
+ retrieveDateTime: new Date('2025-01-15T14:00:00.000Z'),
+ },
+ requestId: testRequestId,
+ }
+
+ const response = testableService.handleRequestUpdateFirmware(station, request)
+
+ assert.strictEqual(response.status, UpdateFirmwareStatusEnumType.Accepted)
+ assert.strictEqual(typeof response.status, 'string')
+ })
+
+ await it('should return Accepted for request with retries and retryInterval', () => {
+ const request: OCPP20UpdateFirmwareRequest = {
+ firmware: {
+ installDateTime: new Date('2025-01-15T16:00:00.000Z'),
+ location: 'https://firmware.example.com/update-retry.bin',
+ retrieveDateTime: new Date('2025-01-15T15:00:00.000Z'),
+ },
+ requestId: 5,
+ retries: 3,
+ retryInterval: 60,
+ }
+
+ const response = testableService.handleRequestUpdateFirmware(station, request)
+
+ assert.strictEqual(response.status, UpdateFirmwareStatusEnumType.Accepted)
+ })
+})
--- /dev/null
+/**
+ * @file Tests for OCPP20RequestService FirmwareStatusNotification
+ * @description Unit tests for OCPP 2.0.1 FirmwareStatusNotification outgoing command (J01)
+ */
+
+import assert from 'node:assert/strict'
+import { afterEach, beforeEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../../../src/charging-station/index.js'
+
+import {
+ createTestableRequestService,
+ type SendMessageMock,
+ type TestableOCPP20RequestService,
+} from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
+import {
+ FirmwareStatusEnumType,
+ type OCPP20FirmwareStatusNotificationRequest,
+ type OCPP20FirmwareStatusNotificationResponse,
+ OCPP20RequestCommand,
+ OCPPVersion,
+} from '../../../../src/types/index.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
+import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
+
+await describe('J01 - FirmwareStatusNotification', async () => {
+ let station: ChargingStation
+ let sendMessageMock: SendMessageMock
+ let service: TestableOCPP20RequestService
+
+ beforeEach(() => {
+ const { station: newStation } = createMockChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 1,
+ evseConfiguration: { evsesCount: 1 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ station = newStation
+
+ const testable = createTestableRequestService<OCPP20FirmwareStatusNotificationResponse>({
+ sendMessageResponse: {},
+ })
+ sendMessageMock = testable.sendMessageMock
+ service = testable.service
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ await it('should send FirmwareStatusNotification with Downloading status', async () => {
+ await service.requestFirmwareStatusNotification(station, FirmwareStatusEnumType.Downloading, 42)
+
+ assert.strictEqual(sendMessageMock.mock.calls.length, 1)
+
+ const sentPayload = sendMessageMock.mock.calls[0]
+ .arguments[2] as OCPP20FirmwareStatusNotificationRequest
+ assert.strictEqual(sentPayload.status, FirmwareStatusEnumType.Downloading)
+ assert.strictEqual(sentPayload.requestId, 42)
+
+ const commandName = sendMessageMock.mock.calls[0].arguments[3]
+ assert.strictEqual(commandName, OCPP20RequestCommand.FIRMWARE_STATUS_NOTIFICATION)
+ })
+
+ await it('should include requestId when provided', async () => {
+ const testRequestId = 99
+
+ await service.requestFirmwareStatusNotification(
+ station,
+ FirmwareStatusEnumType.Installed,
+ testRequestId
+ )
+
+ assert.strictEqual(sendMessageMock.mock.calls.length, 1)
+
+ const sentPayload = sendMessageMock.mock.calls[0]
+ .arguments[2] as OCPP20FirmwareStatusNotificationRequest
+ assert.strictEqual(sentPayload.status, FirmwareStatusEnumType.Installed)
+ assert.strictEqual(sentPayload.requestId, testRequestId)
+ })
+
+ await it('should return empty response from CSMS', async () => {
+ const response = await service.requestFirmwareStatusNotification(
+ station,
+ FirmwareStatusEnumType.Downloaded,
+ 1
+ )
+
+ assert.notStrictEqual(response, undefined)
+ assert.strictEqual(typeof response, 'object')
+ })
+})
--- /dev/null
+/**
+ * @file Tests for OCPP20RequestService LogStatusNotification
+ * @description Unit tests for OCPP 2.0.1 LogStatusNotification outgoing command (M04)
+ */
+
+import assert from 'node:assert/strict'
+import { afterEach, beforeEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../../../src/charging-station/index.js'
+
+import {
+ createTestableRequestService,
+ type SendMessageMock,
+ type TestableOCPP20RequestService,
+} from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
+import {
+ type OCPP20LogStatusNotificationRequest,
+ type OCPP20LogStatusNotificationResponse,
+ OCPP20RequestCommand,
+ OCPPVersion,
+ UploadLogStatusEnumType,
+} from '../../../../src/types/index.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
+import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
+
+await describe('M04 - LogStatusNotification', async () => {
+ let station: ChargingStation
+ let sendMessageMock: SendMessageMock
+ let service: TestableOCPP20RequestService
+
+ beforeEach(() => {
+ const { station: newStation } = createMockChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 1,
+ evseConfiguration: { evsesCount: 1 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ station = newStation
+
+ const testable = createTestableRequestService<OCPP20LogStatusNotificationResponse>({
+ sendMessageResponse: {},
+ })
+ sendMessageMock = testable.sendMessageMock
+ service = testable.service
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ await it('should send LogStatusNotification with Uploading status', async () => {
+ await service.requestLogStatusNotification(station, UploadLogStatusEnumType.Uploading, 42)
+
+ assert.strictEqual(sendMessageMock.mock.calls.length, 1)
+
+ const sentPayload = sendMessageMock.mock.calls[0]
+ .arguments[2] as OCPP20LogStatusNotificationRequest
+ assert.strictEqual(sentPayload.status, UploadLogStatusEnumType.Uploading)
+ assert.strictEqual(sentPayload.requestId, 42)
+
+ const commandName = sendMessageMock.mock.calls[0].arguments[3]
+ assert.strictEqual(commandName, OCPP20RequestCommand.LOG_STATUS_NOTIFICATION)
+ })
+
+ await it('should include requestId when provided', async () => {
+ const testRequestId = 99
+
+ await service.requestLogStatusNotification(
+ station,
+ UploadLogStatusEnumType.Uploaded,
+ testRequestId
+ )
+
+ assert.strictEqual(sendMessageMock.mock.calls.length, 1)
+
+ const sentPayload = sendMessageMock.mock.calls[0]
+ .arguments[2] as OCPP20LogStatusNotificationRequest
+ assert.strictEqual(sentPayload.status, UploadLogStatusEnumType.Uploaded)
+ assert.strictEqual(sentPayload.requestId, testRequestId)
+ })
+
+ await it('should return empty response from CSMS', async () => {
+ const response = await service.requestLogStatusNotification(
+ station,
+ UploadLogStatusEnumType.Uploading,
+ 1
+ )
+
+ assert.notStrictEqual(response, undefined)
+ assert.strictEqual(typeof response, 'object')
+ })
+})
--- /dev/null
+/**
+ * @file Tests for OCPP20RequestService MeterValues
+ * @description Unit tests for OCPP 2.0.1 MeterValues outgoing command (G01)
+ */
+
+import assert from 'node:assert/strict'
+import { afterEach, beforeEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../../../src/charging-station/index.js'
+
+import {
+ createTestableRequestService,
+ type SendMessageMock,
+ type TestableOCPP20RequestService,
+} from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
+import {
+ type OCPP20MeterValuesRequest,
+ type OCPP20MeterValuesResponse,
+ OCPP20RequestCommand,
+ OCPPVersion,
+} from '../../../../src/types/index.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
+import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
+
+await describe('G01 - MeterValues', async () => {
+ let station: ChargingStation
+ let sendMessageMock: SendMessageMock
+ let service: TestableOCPP20RequestService
+
+ beforeEach(() => {
+ const { station: newStation } = createMockChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 1,
+ evseConfiguration: { evsesCount: 1 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ station = newStation
+
+ const testable = createTestableRequestService<OCPP20MeterValuesResponse>({
+ sendMessageResponse: {},
+ })
+ sendMessageMock = testable.sendMessageMock
+ service = testable.service
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ await it('should send MeterValues with valid EVSE ID and meter values', async () => {
+ const testTimestamp = new Date('2024-06-01T12:00:00.000Z')
+ const evseId = 1
+ const meterValue = [
+ {
+ sampledValue: [{ value: 1500.5 }],
+ timestamp: testTimestamp,
+ },
+ ]
+
+ await service.requestMeterValues(station, evseId, meterValue)
+
+ assert.strictEqual(sendMessageMock.mock.calls.length, 1)
+
+ const sentPayload = sendMessageMock.mock.calls[0].arguments[2] as OCPP20MeterValuesRequest
+ assert.strictEqual(sentPayload.evseId, evseId)
+ assert.strictEqual(sentPayload.meterValue.length, 1)
+ assert.strictEqual(sentPayload.meterValue[0].sampledValue[0].value, 1500.5)
+ assert.strictEqual(sentPayload.meterValue[0].timestamp, testTimestamp)
+
+ const commandName = sendMessageMock.mock.calls[0].arguments[3]
+ assert.strictEqual(commandName, OCPP20RequestCommand.METER_VALUES)
+ })
+
+ await it('should send MeterValues with multiple sampledValue entries', async () => {
+ const testTimestamp = new Date('2024-06-01T12:05:00.000Z')
+ const evseId = 2
+ const meterValue = [
+ {
+ sampledValue: [{ value: 230.1 }, { value: 16.0 }, { value: 3680.0 }],
+ timestamp: testTimestamp,
+ },
+ ]
+
+ await service.requestMeterValues(station, evseId, meterValue)
+
+ assert.strictEqual(sendMessageMock.mock.calls.length, 1)
+
+ const sentPayload = sendMessageMock.mock.calls[0].arguments[2] as OCPP20MeterValuesRequest
+ assert.strictEqual(sentPayload.meterValue[0].sampledValue.length, 3)
+ assert.strictEqual(sentPayload.meterValue[0].sampledValue[0].value, 230.1)
+ assert.strictEqual(sentPayload.meterValue[0].sampledValue[1].value, 16.0)
+ assert.strictEqual(sentPayload.meterValue[0].sampledValue[2].value, 3680.0)
+ })
+
+ await it('should set evseId correctly including zero for main power meter', async () => {
+ const testTimestamp = new Date('2024-06-01T12:10:00.000Z')
+ const meterValue = [
+ {
+ sampledValue: [{ value: 50000.0 }],
+ timestamp: testTimestamp,
+ },
+ ]
+
+ // evseId 0 = main power meter
+ const response = await service.requestMeterValues(station, 0, meterValue)
+
+ assert.strictEqual(sendMessageMock.mock.calls.length, 1)
+
+ const sentPayload = sendMessageMock.mock.calls[0].arguments[2] as OCPP20MeterValuesRequest
+ assert.strictEqual(sentPayload.evseId, 0)
+
+ assert.notStrictEqual(response, undefined)
+ assert.strictEqual(typeof response, 'object')
+ })
+})
--- /dev/null
+/**
+ * @file Tests for OCPP20RequestService SecurityEventNotification
+ * @description Unit tests for OCPP 2.0 SecurityEventNotification outgoing command (A04)
+ */
+import assert from 'node:assert/strict'
+import { afterEach, beforeEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../../../src/charging-station/index.js'
+
+import {
+ createTestableRequestService,
+ type SendMessageMock,
+ type TestableOCPP20RequestService,
+} from '../../../../src/charging-station/ocpp/2.0/__testable__/index.js'
+import {
+ OCPP20RequestCommand,
+ type OCPP20SecurityEventNotificationRequest,
+ type OCPP20SecurityEventNotificationResponse,
+ OCPPVersion,
+} from '../../../../src/types/index.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
+import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
+
+await describe('A04 - SecurityEventNotification', async () => {
+ let station: ChargingStation
+ let sendMessageMock: SendMessageMock
+ let service: TestableOCPP20RequestService
+
+ beforeEach(() => {
+ const { station: newStation } = createMockChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 1,
+ evseConfiguration: { evsesCount: 1 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ station = newStation
+
+ const testable = createTestableRequestService<OCPP20SecurityEventNotificationResponse>({
+ sendMessageResponse: {},
+ })
+ sendMessageMock = testable.sendMessageMock
+ service = testable.service
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ // FR: A04.FR.01
+ await it('should send SecurityEventNotification with type and timestamp', async () => {
+ const testTimestamp = new Date('2024-03-15T10:30:00.000Z')
+ const testType = 'FirmwareUpdated'
+
+ await service.requestSecurityEventNotification(station, testType, testTimestamp)
+
+ assert.strictEqual(sendMessageMock.mock.calls.length, 1)
+
+ const sentPayload = sendMessageMock.mock.calls[0]
+ .arguments[2] as OCPP20SecurityEventNotificationRequest
+ assert.strictEqual(sentPayload.type, testType)
+ assert.strictEqual(sentPayload.timestamp, testTimestamp)
+ assert.strictEqual(sentPayload.techInfo, undefined)
+
+ const commandName = sendMessageMock.mock.calls[0].arguments[3]
+ assert.strictEqual(commandName, OCPP20RequestCommand.SECURITY_EVENT_NOTIFICATION)
+ })
+
+ // FR: A04.FR.02
+ await it('should include techInfo when provided', async () => {
+ const testTimestamp = new Date('2024-03-15T11:00:00.000Z')
+ const testType = 'TamperDetectionActivated'
+ const testTechInfo = 'Enclosure opened at connector 1'
+
+ await service.requestSecurityEventNotification(station, testType, testTimestamp, testTechInfo)
+
+ assert.strictEqual(sendMessageMock.mock.calls.length, 1)
+
+ const sentPayload = sendMessageMock.mock.calls[0]
+ .arguments[2] as OCPP20SecurityEventNotificationRequest
+ assert.strictEqual(sentPayload.type, testType)
+ assert.strictEqual(sentPayload.timestamp, testTimestamp)
+ assert.strictEqual(sentPayload.techInfo, testTechInfo)
+ })
+
+ // FR: A04.FR.03
+ await it('should return empty response from CSMS', async () => {
+ const response = await service.requestSecurityEventNotification(
+ station,
+ 'SettingSystemTime',
+ new Date('2024-03-15T12:00:00.000Z')
+ )
+
+ assert.notStrictEqual(response, undefined)
+ assert.strictEqual(typeof response, 'object')
+ })
+})
/**
* @file Tests for OCPP20ResponseService TransactionEvent response handling
- * @description Unit tests for OCPP 2.0 TransactionEvent response processing (E01-E04)
- *
- * Covers:
- * - E01-E04 TransactionEventResponse handler branch coverage
- * - Empty response (no optional fields) — baseline
- * - totalCost logging branch
- * - chargingPriority logging branch
- * - idTokenInfo.Accepted logging branch
- * - idTokenInfo.Invalid logging branch
- * - updatedPersonalMessage logging branch
- * - All fields together
+ * @description Unit tests for OCPP 2.0.1 TransactionEvent response processing including
+ * idTokenInfo.status enforcement per spec D01/D05 — rejected statuses must trigger transaction stop
*/
import assert from 'node:assert/strict'
import { afterEach, beforeEach, describe, it, mock } from 'node:test'
-import type { MockChargingStation } from '../../ChargingStationTestUtils.js'
+import type { ChargingStation } from '../../../../src/charging-station/index.js'
+import type {
+ OCPP20TransactionEventRequest,
+ OCPP20TransactionEventResponse,
+} from '../../../../src/types/index.js'
+import type { UUIDv4 } from '../../../../src/types/UUID.js'
import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js'
-import { OCPP20RequestCommand, OCPPVersion } from '../../../../src/types/index.js'
+import { OCPP20ServiceUtils } from '../../../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js'
+import { OCPPVersion } from '../../../../src/types/index.js'
import {
OCPP20AuthorizationStatusEnumType,
- type OCPP20MessageContentType,
OCPP20MessageFormatEnumType,
- type OCPP20TransactionEventResponse,
+ OCPP20TransactionEventEnumType,
+ OCPP20TriggerReasonEnumType,
} from '../../../../src/types/ocpp/2.0/Transaction.js'
import { Constants } from '../../../../src/utils/index.js'
-import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import {
+ setupConnectorWithTransaction,
+ standardCleanup,
+} from '../../../helpers/TestLifecycleHelpers.js'
import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConstants.js'
import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
+/** UUID used as transactionId in all tests — must match connector.transactionId */
+const TEST_TRANSACTION_ID: UUIDv4 = '00000000-0000-0000-0000-000000000001'
+
+interface TestableOCPP20ResponseService {
+ handleResponseTransactionEvent: (
+ chargingStation: ChargingStation,
+ payload: OCPP20TransactionEventResponse,
+ requestPayload: OCPP20TransactionEventRequest
+ ) => void
+}
+
/**
- * Create a mock station suitable for TransactionEvent response tests.
- * Uses ocppStrictCompliance: false to bypass AJV validation so the
- * handler logic can be tested in isolation.
- * @returns A mock station configured for TransactionEvent tests
+ * Builds a minimal OCPP20TransactionEventRequest for use as requestPayload in tests.
+ * @param transactionId - The transaction UUID to embed in transactionInfo
+ * @returns A minimal OCPP20TransactionEventRequest
*/
-function createTransactionEventStation (): MockChargingStation {
- const { station } = createMockChargingStation({
- baseName: TEST_CHARGING_STATION_BASE_NAME,
- connectorsCount: 1,
- heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
- stationInfo: {
- // Bypass AJV schema validation — tests focus on handler logic
- ocppStrictCompliance: false,
- ocppVersion: OCPPVersion.VERSION_201,
+function buildTransactionEventRequest (transactionId: UUIDv4): OCPP20TransactionEventRequest {
+ return {
+ eventType: OCPP20TransactionEventEnumType.Updated,
+ meterValue: [],
+ seqNo: 0,
+ timestamp: new Date(),
+ transactionInfo: {
+ transactionId,
},
- websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
- })
- return station as MockChargingStation
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ }
+}
+
+/**
+ * Creates a testable wrapper around OCPP20ResponseService.
+ * @param service - The OCPP20ResponseService instance to wrap
+ * @returns A typed interface exposing private handler methods
+ */
+function createTestableResponseService (
+ service: OCPP20ResponseService
+): TestableOCPP20ResponseService {
+ const serviceImpl = service as unknown as TestableOCPP20ResponseService
+ return {
+ handleResponseTransactionEvent: serviceImpl.handleResponseTransactionEvent.bind(service),
+ }
}
-await describe('E01-E04 - TransactionEventResponse handler', async () => {
- let responseService: OCPP20ResponseService
- let mockStation: MockChargingStation
+await describe('D01 - TransactionEvent Response', async () => {
+ let station: ChargingStation
+ let testable: TestableOCPP20ResponseService
beforeEach(() => {
- mock.timers.enable({ apis: ['setInterval', 'setTimeout'] })
- responseService = new OCPP20ResponseService()
- mockStation = createTransactionEventStation()
+ const { station: mockStation } = createMockChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 1,
+ evseConfiguration: { evsesCount: 1 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ station = mockStation
+ // Set connector transactionId to the UUID string used in request payloads
+ setupConnectorWithTransaction(station, 1, { transactionId: 100 })
+ // Override with UUID string so getConnectorIdByTransactionId can find it
+ const connector = station.getConnectorStatus(1)
+ if (connector != null) {
+ connector.transactionId = TEST_TRANSACTION_ID
+ }
+ const responseService = new OCPP20ResponseService()
+ testable = createTestableResponseService(responseService)
})
afterEach(() => {
standardCleanup()
})
- /**
- * Helper to dispatch a TransactionEventResponse through the public responseHandler.
- * The station is in Accepted state by default (RegistrationStatusEnumType.ACCEPTED).
- * @param payload - The TransactionEventResponse payload to dispatch
- * @returns Resolves when the response handler completes
- */
- async function dispatch (payload: OCPP20TransactionEventResponse): Promise<void> {
- await responseService.responseHandler(
- mockStation,
- OCPP20RequestCommand.TRANSACTION_EVENT,
- payload as unknown as Parameters<typeof responseService.responseHandler>[2],
- {} as Parameters<typeof responseService.responseHandler>[3]
+ await it('should not stop transaction when idTokenInfo status is Accepted', () => {
+ // Arrange
+ const mockStopTransaction = mock.method(OCPP20ServiceUtils, 'requestStopTransaction', () =>
+ Promise.resolve({ status: 'Accepted' })
)
- }
+ const payload: OCPP20TransactionEventResponse = {
+ idTokenInfo: {
+ status: OCPP20AuthorizationStatusEnumType.Accepted,
+ },
+ }
+ const requestPayload = buildTransactionEventRequest(TEST_TRANSACTION_ID)
- await it('should handle empty TransactionEvent response without throwing', async () => {
- const payload: OCPP20TransactionEventResponse = {}
- await assert.doesNotReject(dispatch(payload))
+ // Act
+ testable.handleResponseTransactionEvent(station, payload, requestPayload)
+
+ // Assert
+ assert.strictEqual(mockStopTransaction.mock.calls.length, 0)
})
- await it('should handle totalCost field without throwing', async () => {
- const payload: OCPP20TransactionEventResponse = { totalCost: 12.5 }
- await assert.doesNotReject(dispatch(payload))
+ await it('should stop only the specific transaction when idTokenInfo status is Invalid', () => {
+ // Arrange
+ const mockStopTransaction = mock.method(OCPP20ServiceUtils, 'requestStopTransaction', () =>
+ Promise.resolve({ status: 'Accepted' })
+ )
+ const payload: OCPP20TransactionEventResponse = {
+ idTokenInfo: {
+ status: OCPP20AuthorizationStatusEnumType.Invalid,
+ },
+ }
+ const requestPayload = buildTransactionEventRequest(TEST_TRANSACTION_ID)
+
+ // Act
+ testable.handleResponseTransactionEvent(station, payload, requestPayload)
+
+ // Assert — only the specific connector (1) on EVSE (1) is stopped
+ assert.strictEqual(mockStopTransaction.mock.calls.length, 1)
+ assert.strictEqual(mockStopTransaction.mock.calls[0].arguments[0], station)
+ assert.strictEqual(mockStopTransaction.mock.calls[0].arguments[1], 1)
+ assert.strictEqual(mockStopTransaction.mock.calls[0].arguments[2], 1)
})
- await it('should handle chargingPriority field without throwing', async () => {
- const payload: OCPP20TransactionEventResponse = { chargingPriority: 1 }
- await assert.doesNotReject(dispatch(payload))
+ await it('should stop only the specific transaction when idTokenInfo status is Blocked', () => {
+ // Arrange
+ const mockStopTransaction = mock.method(OCPP20ServiceUtils, 'requestStopTransaction', () =>
+ Promise.resolve({ status: 'Accepted' })
+ )
+ const payload: OCPP20TransactionEventResponse = {
+ idTokenInfo: {
+ status: OCPP20AuthorizationStatusEnumType.Blocked,
+ },
+ }
+ const requestPayload = buildTransactionEventRequest(TEST_TRANSACTION_ID)
+
+ // Act
+ testable.handleResponseTransactionEvent(station, payload, requestPayload)
+
+ // Assert
+ assert.strictEqual(mockStopTransaction.mock.calls.length, 1)
+ assert.strictEqual(mockStopTransaction.mock.calls[0].arguments[0], station)
})
- await it('should handle idTokenInfo with Accepted status without throwing', async () => {
+ await it('should not stop transaction when only chargingPriority is present', () => {
+ // Arrange
+ const mockStopTransaction = mock.method(OCPP20ServiceUtils, 'requestStopTransaction', () =>
+ Promise.resolve({ status: 'Accepted' })
+ )
+ const payload: OCPP20TransactionEventResponse = {
+ chargingPriority: 5,
+ }
+ const requestPayload = buildTransactionEventRequest(TEST_TRANSACTION_ID)
+
+ // Act
+ testable.handleResponseTransactionEvent(station, payload, requestPayload)
+
+ // Assert
+ assert.strictEqual(mockStopTransaction.mock.calls.length, 0)
+ })
+
+ await it('should handle empty response without stopping transaction', () => {
+ // Arrange
+ const mockStopTransaction = mock.method(OCPP20ServiceUtils, 'requestStopTransaction', () =>
+ Promise.resolve({ status: 'Accepted' })
+ )
+ const payload: OCPP20TransactionEventResponse = {}
+ const requestPayload = buildTransactionEventRequest(TEST_TRANSACTION_ID)
+
+ // Act
+ testable.handleResponseTransactionEvent(station, payload, requestPayload)
+
+ // Assert
+ assert.strictEqual(mockStopTransaction.mock.calls.length, 0)
+ })
+
+ await it('should stop only the specific transaction when idTokenInfo status is Expired', () => {
+ // Arrange
+ const mockStopTransaction = mock.method(OCPP20ServiceUtils, 'requestStopTransaction', () =>
+ Promise.resolve({ status: 'Accepted' })
+ )
const payload: OCPP20TransactionEventResponse = {
idTokenInfo: {
- status: OCPP20AuthorizationStatusEnumType.Accepted,
+ status: OCPP20AuthorizationStatusEnumType.Expired,
},
}
- await assert.doesNotReject(dispatch(payload))
+ const requestPayload = buildTransactionEventRequest(TEST_TRANSACTION_ID)
+
+ // Act
+ testable.handleResponseTransactionEvent(station, payload, requestPayload)
+
+ // Assert
+ assert.strictEqual(mockStopTransaction.mock.calls.length, 1)
})
- await it('should handle idTokenInfo with Invalid status without throwing', async () => {
+ await it('should stop only the specific transaction when idTokenInfo status is NoCredit', () => {
+ // Arrange
+ const mockStopTransaction = mock.method(OCPP20ServiceUtils, 'requestStopTransaction', () =>
+ Promise.resolve({ status: 'Accepted' })
+ )
const payload: OCPP20TransactionEventResponse = {
idTokenInfo: {
- status: OCPP20AuthorizationStatusEnumType.Invalid,
+ status: OCPP20AuthorizationStatusEnumType.NoCredit,
},
}
- await assert.doesNotReject(dispatch(payload))
+ const requestPayload = buildTransactionEventRequest(TEST_TRANSACTION_ID)
+
+ // Act
+ testable.handleResponseTransactionEvent(station, payload, requestPayload)
+
+ // Assert
+ assert.strictEqual(mockStopTransaction.mock.calls.length, 1)
})
- await it('should handle updatedPersonalMessage field without throwing', async () => {
- const message: OCPP20MessageContentType = {
- content: 'Thank you for charging!',
- format: OCPP20MessageFormatEnumType.UTF8,
+ await it('should not stop transaction when response has totalCost and updatedPersonalMessage', () => {
+ // Arrange
+ const mockStopTransaction = mock.method(OCPP20ServiceUtils, 'requestStopTransaction', () =>
+ Promise.resolve({ status: 'Accepted' })
+ )
+ const payload: OCPP20TransactionEventResponse = {
+ totalCost: 12.5,
+ updatedPersonalMessage: {
+ content: 'Charging session in progress',
+ format: OCPP20MessageFormatEnumType.UTF8,
+ },
}
- const payload: OCPP20TransactionEventResponse = { updatedPersonalMessage: message }
- await assert.doesNotReject(dispatch(payload))
+ const requestPayload = buildTransactionEventRequest(TEST_TRANSACTION_ID)
+
+ // Act
+ testable.handleResponseTransactionEvent(station, payload, requestPayload)
+
+ // Assert
+ assert.strictEqual(mockStopTransaction.mock.calls.length, 0)
})
- await it('should handle all optional fields present simultaneously without throwing', async () => {
- const message: OCPP20MessageContentType = {
- content: '<b>Session complete</b>',
- format: OCPP20MessageFormatEnumType.HTML,
+ await it('should stop only the targeted transaction on multi-EVSE station', () => {
+ // Set up a 2-EVSE station with active transactions on both EVSEs
+ const txn1: UUIDv4 = '00000000-0000-0000-0000-000000000010'
+ const txn2: UUIDv4 = '00000000-0000-0000-0000-000000000020'
+ const { station: multiStation } = createMockChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 2,
+ evseConfiguration: { evsesCount: 2 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ setupConnectorWithTransaction(multiStation, 1, { transactionId: 10 })
+ const connector1 = multiStation.getConnectorStatus(1)
+ if (connector1 != null) {
+ connector1.transactionId = txn1
}
+ setupConnectorWithTransaction(multiStation, 2, { transactionId: 20 })
+ const connector2 = multiStation.getConnectorStatus(2)
+ if (connector2 != null) {
+ connector2.transactionId = txn2
+ }
+
+ const mockStopTransaction = mock.method(OCPP20ServiceUtils, 'requestStopTransaction', () =>
+ Promise.resolve({ status: 'Accepted' })
+ )
const payload: OCPP20TransactionEventResponse = {
- chargingPriority: 2,
idTokenInfo: {
- chargingPriority: 3,
- status: OCPP20AuthorizationStatusEnumType.Accepted,
+ status: OCPP20AuthorizationStatusEnumType.Invalid,
},
- totalCost: 9.99,
- updatedPersonalMessage: message,
}
- await assert.doesNotReject(dispatch(payload))
+ const multiTestable = createTestableResponseService(new OCPP20ResponseService())
+
+ // Act — reject EVSE 1's transaction only
+ multiTestable.handleResponseTransactionEvent(
+ multiStation,
+ payload,
+ buildTransactionEventRequest(txn1)
+ )
+
+ // Assert — only 1 stop call targeting connector 1, EVSE 2 untouched
+ assert.strictEqual(mockStopTransaction.mock.calls.length, 1)
+ assert.strictEqual(mockStopTransaction.mock.calls[0].arguments[0], multiStation)
+ assert.strictEqual(mockStopTransaction.mock.calls[0].arguments[1], 1)
})
})