ConnectorStatusEnum,
DataEnumType,
ErrorType,
+ FirmwareStatus,
GenericDeviceModelStatusEnumType,
GenericStatus,
GetVariableStatusEnumType,
type IncomingRequestHandler,
type JsonType,
+ type OCPP20ClearCacheResponse,
OCPP20ComponentName,
OCPP20ConnectorStatusEnumType,
OCPP20DeviceInfoVariableName,
validateIdentifierString,
} from '../../../utils/index.js'
import { getConfigurationKey } from '../../ConfigurationKeyUtils.js'
-import { resetConnectorStatus } from '../../Helpers.js'
+import { hasReservationExpired, resetConnectorStatus } from '../../Helpers.js'
+import { OCPPAuthServiceFactory } from '../auth/services/OCPPAuthServiceFactory.js'
import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService.js'
import {
OCPPServiceUtils,
restoreConnectorStatus,
sendAndSetConnectorStatus,
} from '../OCPPServiceUtils.js'
+import { OCPP20Constants } from './OCPP20Constants.js'
import { OCPP20ServiceUtils } from './OCPP20ServiceUtils.js'
import { OCPP20VariableManager } from './OCPP20VariableManager.js'
import { getVariableMetadata, VARIABLE_REGISTRY } from './OCPP20VariableRegistry.js'
this.incomingRequestHandlers = new Map<OCPP20IncomingRequestCommand, IncomingRequestHandler>([
[
OCPP20IncomingRequestCommand.CLEAR_CACHE,
- super.handleRequestClearCache.bind(this) as IncomingRequestHandler,
+ this.handleRequestClearCache.bind(this) as unknown as IncomingRequestHandler,
],
[
OCPP20IncomingRequestCommand.GET_BASE_REPORT,
}
}
+ /**
+ * Handles OCPP 2.0.1 ClearCache request by clearing the Authorization Cache
+ * per OCPP 2.0.1 spec C11.FR.01
+ * Per C11.FR.04: Returns Rejected if AuthCacheEnabled is false
+ * @param chargingStation - The charging station instance
+ * @returns Promise resolving to ClearCacheResponse
+ */
+ protected override async handleRequestClearCache (
+ chargingStation: ChargingStation
+ ): Promise<OCPP20ClearCacheResponse> {
+ try {
+ const authService = await OCPPAuthServiceFactory.getInstance(chargingStation)
+ // C11.FR.04: IF AuthCacheEnabled is false, CS SHALL send ClearCacheResponse with status Rejected
+ const config = authService.getConfiguration()
+ if (!config.authorizationCacheEnabled) {
+ logger.info(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestClearCache: Authorization cache disabled, returning Rejected (C11.FR.04)`
+ )
+ return OCPP20Constants.OCPP_RESPONSE_REJECTED
+ }
+ await authService.clearCache()
+ logger.info(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestClearCache: Authorization cache cleared`
+ )
+ return OCPP20Constants.OCPP_RESPONSE_ACCEPTED
+ } catch (error) {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestClearCache: Error clearing cache:`,
+ error
+ )
+ return OCPP20Constants.OCPP_RESPONSE_REJECTED
+ }
+ }
+
private buildReportData (
chargingStation: ChargingStation,
reportBase: ReportBaseEnumType
* @param evseId - The EVSE identifier to reset
*/
private scheduleEvseResetOnIdle (chargingStation: ChargingStation, evseId: number): void {
- // Monitor for transaction completion and reset when idle
+ // Monitor for idle state per Errata 2.14 and reset when idle
const monitorInterval = setInterval(() => {
const evse = chargingStation.evses.get(evseId)
if (evse) {
+ // Check all idle conditions per Errata 2.14 (OnIdle definition):
+ // 1. Active transactions on EVSE
let hasActiveTransactions = false
+ let hasPendingReservation = false
for (const [, connector] of evse.connectors) {
+ // Check for active transaction
if (connector.transactionId !== undefined) {
hasActiveTransactions = true
- break
+ }
+ // Check for pending (non-expired) reservation
+ if (connector.reservation != null && !hasReservationExpired(connector.reservation)) {
+ hasPendingReservation = true
}
}
- if (!hasActiveTransactions) {
+ // 2. Firmware update in progress (station-wide affects all EVSEs)
+ const firmwareStatus = chargingStation.stationInfo?.firmwareStatus
+ const hasFirmwareUpdateInProgress =
+ firmwareStatus === FirmwareStatus.Downloading ||
+ firmwareStatus === FirmwareStatus.Downloaded ||
+ firmwareStatus === FirmwareStatus.Installing
+
+ // Note: Log uploads and cable lock state are not tracked in the simulator.
+ // Per Errata 2.14, these would also prevent idle state, but the simulator
+ // does not implement log upload tracking or separate cable lock state.
+
+ const isIdle =
+ !hasActiveTransactions && !hasFirmwareUpdateInProgress && !hasPendingReservation
+
+ if (isIdle) {
clearInterval(monitorInterval)
logger.info(
`${chargingStation.logPrefix()} ${moduleName}.scheduleEvseResetOnIdle: EVSE ${evseId.toString()} is now idle, executing reset`
* @param chargingStation - The charging station instance
*/
private scheduleResetOnIdle (chargingStation: ChargingStation): void {
- // Monitor for transaction completion and reset when idle
+ // Monitor for idle state per Errata 2.14 and reset when idle
const monitorInterval = setInterval(() => {
+ // Check all idle conditions per Errata 2.14 (OnIdle definition):
+ // 1. Active transactions
const hasActiveTransactions = chargingStation.getNumberOfRunningTransactions() > 0
- if (!hasActiveTransactions) {
+ // 2. Firmware update in progress
+ const firmwareStatus = chargingStation.stationInfo?.firmwareStatus
+ const hasFirmwareUpdateInProgress =
+ firmwareStatus === FirmwareStatus.Downloading ||
+ firmwareStatus === FirmwareStatus.Downloaded ||
+ firmwareStatus === FirmwareStatus.Installing
+
+ // 3. Pending reservations (non-expired)
+ let hasPendingReservation = false
+ if (chargingStation.hasEvses) {
+ for (const evse of chargingStation.evses.values()) {
+ for (const connector of evse.connectors.values()) {
+ if (connector.reservation != null && !hasReservationExpired(connector.reservation)) {
+ hasPendingReservation = true
+ break
+ }
+ }
+ if (hasPendingReservation) break
+ }
+ } else {
+ for (const connector of chargingStation.connectors.values()) {
+ if (connector.reservation != null && !hasReservationExpired(connector.reservation)) {
+ hasPendingReservation = true
+ break
+ }
+ }
+ }
+
+ // Note: Log uploads and cable lock state are not tracked in the simulator.
+ // Per Errata 2.14, these would also prevent idle state, but the simulator
+ // does not implement log upload tracking or separate cable lock state.
+
+ const isIdle =
+ !hasActiveTransactions && !hasFirmwareUpdateInProgress && !hasPendingReservation
+
+ if (isIdle) {
clearInterval(monitorInterval)
logger.info(
`${chargingStation.logPrefix()} ${moduleName}.scheduleResetOnIdle: Charging station is now idle, executing reset`
import { describe, it } from 'node:test'
import { OCPP20IncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.js'
-import { OCPPVersion } from '../../../../src/types/index.js'
+import { OCPPAuthServiceFactory } from '../../../../src/charging-station/ocpp/auth/services/OCPPAuthServiceFactory.js'
+import { GenericStatus, OCPPVersion } from '../../../../src/types/index.js'
import { Constants } from '../../../../src/utils/index.js'
import { createChargingStation } from '../../../ChargingStationFactory.js'
import { TEST_CHARGING_STATION_BASE_NAME } from './OCPP20TestConstants.js'
const incomingRequestService = new OCPP20IncomingRequestService()
- // FR: C11.FR.01
+ // FR: C11.FR.01 - CS SHALL attempt to clear its Authorization Cache
await it('Should handle ClearCache request successfully', async () => {
const response = await (incomingRequestService as any).handleRequestClearCache(
mockChargingStation
expect(typeof response).toBe('object')
expect(response.status).toBeDefined()
expect(typeof response.status).toBe('string')
- expect(['Accepted', 'Rejected']).toContain(response.status)
+ expect([GenericStatus.Accepted, GenericStatus.Rejected]).toContain(response.status)
})
- // FR: C11.FR.02
+ // FR: C11.FR.02 - Return correct status based on cache clearing result
await it('Should return correct status based on cache clearing result', async () => {
- // Test the actual behavior - ClearCache should work with ID tags cache
-
const response = await (incomingRequestService as any).handleRequestClearCache(
mockChargingStation
)
expect(response).toBeDefined()
expect(response.status).toBeDefined()
// Should be either Accepted or Rejected based on cache state
- expect(['Accepted', 'Rejected']).toContain(response.status)
+ expect([GenericStatus.Accepted, GenericStatus.Rejected]).toContain(response.status)
+ })
+
+ // CLR-001: Verify Authorization Cache is cleared (not IdTagsCache)
+ await describe('CLR-001 - ClearCache clears Authorization Cache', async () => {
+ await it('Should call authService.clearCache() on ClearCache request', async () => {
+ // Create a mock auth service to verify clearCache is called
+ let clearCacheCalled = false
+ const mockAuthService = {
+ clearCache: (): Promise<void> => {
+ clearCacheCalled = true
+ return Promise.resolve()
+ },
+ getConfiguration: () => ({
+ authorizationCacheEnabled: true,
+ }),
+ }
+
+ // Mock the factory to return our mock auth service
+ const originalGetInstance = OCPPAuthServiceFactory.getInstance.bind(OCPPAuthServiceFactory)
+ ;(OCPPAuthServiceFactory as any).getInstance = (): Promise<typeof mockAuthService> =>
+ Promise.resolve(mockAuthService)
+
+ try {
+ const response = await (incomingRequestService as any).handleRequestClearCache(
+ mockChargingStation
+ )
+
+ expect(clearCacheCalled).toBe(true)
+ expect(response.status).toBe(GenericStatus.Accepted)
+ } finally {
+ // Restore original factory method
+ ;(OCPPAuthServiceFactory as any).getInstance = originalGetInstance
+ }
+ })
+
+ await it('Should NOT call idTagsCache.deleteIdTags() on ClearCache request', async () => {
+ // Verify that IdTagsCache is not touched
+ let deleteIdTagsCalled = false
+ // eslint-disable-next-line @typescript-eslint/unbound-method
+ const originalDeleteIdTags = mockChargingStation.idTagsCache.deleteIdTags
+
+ ;(mockChargingStation.idTagsCache as any).deleteIdTags = () => {
+ deleteIdTagsCalled = true
+ }
+
+ try {
+ await (incomingRequestService as any).handleRequestClearCache(mockChargingStation)
+ expect(deleteIdTagsCalled).toBe(false)
+ } finally {
+ // Restore original method
+ ;(mockChargingStation.idTagsCache as any).deleteIdTags = originalDeleteIdTags
+ }
+ })
+ })
+
+ // CLR-002: Verify AuthCacheEnabled check per C11.FR.04
+ await describe('CLR-002 - AuthCacheEnabled Check (C11.FR.04)', async () => {
+ await it('Should return Rejected when AuthCacheEnabled is false', async () => {
+ // Create a mock auth service with cache disabled
+ const mockAuthService = {
+ clearCache: (): Promise<void> => {
+ throw new Error('clearCache should not be called when cache is disabled')
+ },
+ getConfiguration: () => ({
+ authorizationCacheEnabled: false,
+ }),
+ }
+
+ // Mock the factory to return our mock auth service
+ const originalGetInstance = OCPPAuthServiceFactory.getInstance.bind(OCPPAuthServiceFactory)
+ ;(OCPPAuthServiceFactory as any).getInstance = (): Promise<typeof mockAuthService> =>
+ Promise.resolve(mockAuthService)
+
+ try {
+ const response = await (incomingRequestService as any).handleRequestClearCache(
+ mockChargingStation
+ )
+
+ expect(response.status).toBe(GenericStatus.Rejected)
+ } finally {
+ // Restore original factory method
+ ;(OCPPAuthServiceFactory as any).getInstance = originalGetInstance
+ }
+ })
+
+ await it('Should return Accepted when AuthCacheEnabled is true and clear succeeds', async () => {
+ // Create a mock auth service with cache enabled
+ const mockAuthService = {
+ clearCache: (): Promise<void> => {
+ // Successful clear
+ return Promise.resolve()
+ },
+ getConfiguration: () => ({
+ authorizationCacheEnabled: true,
+ }),
+ }
+
+ // Mock the factory to return our mock auth service
+ const originalGetInstance = OCPPAuthServiceFactory.getInstance.bind(OCPPAuthServiceFactory)
+ ;(OCPPAuthServiceFactory as any).getInstance = (): Promise<typeof mockAuthService> =>
+ Promise.resolve(mockAuthService)
+
+ try {
+ const response = await (incomingRequestService as any).handleRequestClearCache(
+ mockChargingStation
+ )
+
+ expect(response.status).toBe(GenericStatus.Accepted)
+ } finally {
+ // Restore original factory method
+ ;(OCPPAuthServiceFactory as any).getInstance = originalGetInstance
+ }
+ })
+
+ await it('Should return Rejected when clearCache throws an error', async () => {
+ // Create a mock auth service that throws on clearCache
+ const mockAuthService = {
+ clearCache: (): Promise<void> => {
+ return Promise.reject(new Error('Cache clear failed'))
+ },
+ getConfiguration: () => ({
+ authorizationCacheEnabled: true,
+ }),
+ }
+
+ // Mock the factory to return our mock auth service
+ const originalGetInstance = OCPPAuthServiceFactory.getInstance.bind(OCPPAuthServiceFactory)
+ ;(OCPPAuthServiceFactory as any).getInstance = (): Promise<typeof mockAuthService> =>
+ Promise.resolve(mockAuthService)
+
+ try {
+ const response = await (incomingRequestService as any).handleRequestClearCache(
+ mockChargingStation
+ )
+
+ expect(response.status).toBe(GenericStatus.Rejected)
+ } finally {
+ // Restore original factory method
+ ;(OCPPAuthServiceFactory as any).getInstance = originalGetInstance
+ }
+ })
+
+ await it('Should not attempt to clear cache when AuthCacheEnabled is false', async () => {
+ let clearCacheAttempted = false
+ const mockAuthService = {
+ clearCache: (): Promise<void> => {
+ clearCacheAttempted = true
+ return Promise.resolve()
+ },
+ getConfiguration: () => ({
+ authorizationCacheEnabled: false,
+ }),
+ }
+
+ // Mock the factory to return our mock auth service
+ const originalGetInstance = OCPPAuthServiceFactory.getInstance.bind(OCPPAuthServiceFactory)
+ ;(OCPPAuthServiceFactory as any).getInstance = (): Promise<typeof mockAuthService> =>
+ Promise.resolve(mockAuthService)
+
+ try {
+ await (incomingRequestService as any).handleRequestClearCache(mockChargingStation)
+
+ // clearCache should NOT be called when cache is disabled
+ expect(clearCacheAttempted).toBe(false)
+ } finally {
+ // Restore original factory method
+ ;(OCPPAuthServiceFactory as any).getInstance = originalGetInstance
+ }
+ })
})
})