--- /dev/null
+---
+description: Reviews code.
+mode: subagent
+temperature: 0.1
+tools:
+ write: false
+ edit: false
+ bash: false
+---
+
+You are in code review mode. Focus on:
+
+- Code quality
+- Best practices
+- Algorithmic
+- Bugs
+- Edge cases
+- Performance
+- Security
+
+Provide constructive and detailed feedbacks.
--- /dev/null
+---
+description: Run simulator code linter and formatter.
+---
+
+Run simulator code linter and formatter with autofixes.
+Raw output:
+!`pnpm format`
+Summarize code linter or formatter failures and propose targeted fixes.
---
-description: Run simulator tests
+description: Run simulator test suite
---
-Run the simulator test suite.
+Run simulator test suite.
Raw output:
!`pnpm test`
Summarize failing tests and propose targeted fixes.
+++ /dev/null
----
-description: Run simulator tests for a file
----
-
-Run simulator tests filtered by $ARGUMENTS.
-Raw output:
-!`pnpm test -- $ARGUMENTS`
-Summarize failing tests and propose targeted fixes.
- Use `describe()` and `it()` functions from Node.js test runner
- Test files should be named `*.test.ts`
- Use `@std/expect` for assertions
-- Mock charging stations with `createChargingStation()` or `createChargingStationWithEvses()`
+- Mock charging stations with `createChargingStation()`
- Use `/* eslint-disable */` comments for specific test requirements
- Async tests should use `await` in describe/it callbacks
#### E. Transactions
-- :x: RequestStartTransaction
+- :white_check_mark: RequestStartTransaction
- :x: RequestStopTransaction
- :x: TransactionEvent
'shutdowning',
'VCAP',
'workerd',
- // OCPP 2.0.x Component Names
+ // OCPP 2.0.x domain terms
'cppwm',
- // OCPP variable names and domain terms
'heartbeatinterval',
'HEARTBEATINTERVAL',
'websocketpinginterval',
'DEAUTHORIZE',
'deauthorized',
'DEAUTHORIZED',
+ 'Selftest',
+ 'SECC',
+ 'Secc',
+ 'Overcurrent',
],
},
},
case OCPPVersion.VERSION_201:
if (isEmpty(chargingStation.evses)) {
throw new BaseError(
- `${chargingStationId}: OCPP 2.0.x requires at least one EVSE defined in the charging station template/configuration`
+ `${chargingStationId}: OCPP ${chargingStation.stationInfo.ocppVersion} requires at least one EVSE defined in the charging station template/configuration`
)
}
}
)
.map(chargingProfile => {
chargingProfile.chargingSchedule.startSchedule = convertToDate(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
chargingProfile.chargingSchedule.startSchedule
)
chargingProfile.validFrom = convertToDate(chargingProfile.validFrom)
chargingStation.stationInfo!.maximumPower!
if (limit > chargingStationMaximumPower) {
logger.error(
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
`${chargingStation.logPrefix()} ${moduleName}.getChargingStationChargingProfilesLimit: Charging profile id ${chargingProfilesLimit.chargingProfile.chargingProfileId.toString()} limit ${limit.toString()} is greater than charging station maximum ${chargingStationMaximumPower.toString()}: %j`,
chargingProfilesLimit
)
chargingStation.stationInfo!.maximumPower! / chargingStation.powerDivider!
if (limit > connectorMaximumPower) {
logger.error(
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
`${chargingStation.logPrefix()} ${moduleName}.getConnectorChargingProfilesLimit: Charging profile id ${chargingProfilesLimit.chargingProfile.chargingProfileId.toString()} limit ${limit.toString()} is greater than connector ${connectorId.toString()} maximum ${connectorMaximumPower.toString()}: %j`,
chargingProfilesLimit
)
const chargingSchedule = chargingProfile.chargingSchedule
if (chargingSchedule.startSchedule == null) {
logger.debug(
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
`${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId.toString()} has no startSchedule defined. Trying to set it to the connector current transaction start date`
)
// OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
}
if (!isDate(chargingSchedule.startSchedule)) {
logger.warn(
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
`${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId.toString()} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
)
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-non-null-assertion
chargingSchedule.startSchedule = convertToDate(chargingSchedule.startSchedule)!
}
if (chargingSchedule.duration == null) {
logger.debug(
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
`${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId.toString()} has no duration defined and will be set to the maximum time allowed`
)
// OCPP specifies that if duration is not defined, it should be infinite
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
chargingSchedule.duration = differenceInSeconds(maxTime, chargingSchedule.startSchedule)
}
if (
// Check if the charging profile is active
if (
isWithinInterval(currentDate, {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
end: addSeconds(chargingSchedule.startSchedule, chargingSchedule.duration),
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
start: chargingSchedule.startSchedule,
})
) {
): number => a.startPeriod - b.startPeriod
if (
!isArraySorted<ChargingSchedulePeriod>(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
chargingSchedule.chargingSchedulePeriod,
chargingSchedulePeriodCompareFn
)
) {
logger.warn(
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
`${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId.toString()} schedule periods are not sorted by start period`
)
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn)
}
// Check if the first schedule period startPeriod property is equal to 0
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
logger.error(
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
`${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId.toString()} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod.toString()} is not equal to 0`
)
continue
}
// Handle only one schedule period
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (chargingSchedule.chargingSchedulePeriod.length === 1) {
const chargingProfilesLimit: ChargingProfilesLimit = {
chargingProfile,
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
limit: chargingSchedule.chargingSchedulePeriod[0].limit,
}
logger.debug(debugLogMsg, chargingProfilesLimit)
for (const [
index,
chargingSchedulePeriod,
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
] of chargingSchedule.chargingSchedulePeriod.entries()) {
// Find the right schedule period
if (
isAfter(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
addSeconds(chargingSchedule.startSchedule, chargingSchedulePeriod.startPeriod),
currentDate
)
// Found the schedule period: previous is the correct one
const chargingProfilesLimit: ChargingProfilesLimit = {
chargingProfile: previousActiveChargingProfile ?? chargingProfile,
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
limit: previousChargingSchedulePeriod?.limit ?? chargingSchedulePeriod.limit,
}
logger.debug(debugLogMsg, chargingProfilesLimit)
}
// Handle the last schedule period within the charging profile duration
if (
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
differenceInSeconds(
addSeconds(
chargingSchedule.startSchedule,
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/restrict-plus-operands
chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod
),
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
chargingSchedule.startSchedule
) > chargingSchedule.duration)
) {
const chargingProfilesLimit: ChargingProfilesLimit = {
chargingProfile,
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
limit: chargingSchedulePeriod.limit,
}
logger.debug(debugLogMsg, chargingProfilesLimit)
return chargingProfilesLimit
}
// Keep a reference to previous charging schedule period
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
previousChargingSchedulePeriod = chargingSchedulePeriod
}
}
(isValidDate(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo))
) {
logger.debug(
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
`${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId.toString()} is not valid for the current date ${
isDate(currentDate) ? currentDate.toISOString() : currentDate.toString()
}`
chargingProfile.chargingSchedule.duration == null
) {
logger.error(
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
`${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId.toString()} has no startSchedule or duration defined`
)
return false
}
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
if (!isValidDate(chargingProfile.chargingSchedule.startSchedule)) {
logger.error(
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
`${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId.toString()} has an invalid startSchedule date defined`
)
return false
}
if (!Number.isSafeInteger(chargingProfile.chargingSchedule.duration)) {
logger.error(
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
`${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId.toString()} has non integer duration defined`
)
return false
switch (chargingProfile.recurrencyKind) {
case RecurrencyKindType.DAILY:
recurringInterval = {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-non-null-assertion
end: addDays(chargingSchedule.startSchedule!, 1),
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-non-null-assertion
start: chargingSchedule.startSchedule!,
}
checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
!isWithinInterval(currentDate, recurringInterval) &&
isBefore(recurringInterval.end, currentDate)
) {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
chargingSchedule.startSchedule = addDays(
recurringInterval.start,
differenceInDays(currentDate, recurringInterval.start)
)
recurringInterval = {
end: addDays(chargingSchedule.startSchedule, 1),
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
start: chargingSchedule.startSchedule,
}
recurringIntervalTranslated = true
break
case RecurrencyKindType.WEEKLY:
recurringInterval = {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/no-non-null-assertion
end: addWeeks(chargingSchedule.startSchedule!, 1),
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-non-null-assertion
start: chargingSchedule.startSchedule!,
}
checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
!isWithinInterval(currentDate, recurringInterval) &&
isBefore(recurringInterval.end, currentDate)
) {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
chargingSchedule.startSchedule = addWeeks(
recurringInterval.start,
differenceInWeeks(currentDate, recurringInterval.start)
)
recurringInterval = {
end: addWeeks(chargingSchedule.startSchedule, 1),
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
start: chargingSchedule.startSchedule,
}
recurringIntervalTranslated = true
break
default:
logger.error(
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions
`${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${chargingProfile.recurrencyKind} charging profile id ${chargingProfile.chargingProfileId.toString()} is not supported`
)
}
`${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
chargingProfile.recurrencyKind
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
} charging profile id ${chargingProfile.chargingProfileId.toString()} recurrency time interval [${toDate(
recurringInterval?.start as Date
).toISOString()}, ${toDate(
logger.warn(
`${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
chargingProfile.chargingProfileKind
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
} charging profile id ${chargingProfile.chargingProfileId.toString()} duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
interval.end,
interval.start
logger.warn(
`${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
chargingProfile.chargingProfileKind
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
} charging profile id ${chargingProfile.chargingProfileId.toString()} duration ${chargingProfile.chargingSchedule.duration.toString()} is greater than the recurrency time interval duration ${differenceInSeconds(
interval.end,
interval.start
sampledValueTemplate?.unit === OCPP16MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1
meterValue.sampledValue.push(
OCPP16ServiceUtils.buildSampledValue(
+ chargingStation.stationInfo?.ocppVersion,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
sampledValueTemplate!,
roundTo((meterStart ?? 0) / unitDivider, 4),
import type { ValidateFunction } from 'ajv'
import type { ChargingStation } from '../../../charging-station/index.js'
+import type {
+ OCPP20ChargingProfileType,
+ OCPP20IdTokenType,
+} from '../../../types/ocpp/2.0/Transaction.js'
import { OCPPError } from '../../../exception/index.js'
import {
type OCPP20NotifyReportRequest,
type OCPP20NotifyReportResponse,
OCPP20RequestCommand,
+ type OCPP20RequestStartTransactionRequest,
+ type OCPP20RequestStartTransactionResponse,
OCPP20RequiredVariableName,
type OCPP20ResetRequest,
type OCPP20ResetResponse,
ReasonCodeEnumType,
ReportBaseEnumType,
type ReportDataType,
+ RequestStartStopStatusEnumType,
ResetEnumType,
ResetStatusEnumType,
SetVariableStatusEnumType,
StopTransactionReason,
} from '../../../types/index.js'
import { StandardParametersKey } from '../../../types/ocpp/Configuration.js'
-import { convertToIntOrNaN, isAsyncFunction, logger } from '../../../utils/index.js'
+import { convertToIntOrNaN, generateUUID, isAsyncFunction, logger } from '../../../utils/index.js'
import { getConfigurationKey } from '../../ConfigurationKeyUtils.js'
import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService.js'
+import { sendAndSetConnectorStatus } from '../OCPPServiceUtils.js'
import { OCPP20ServiceUtils } from './OCPP20ServiceUtils.js'
import { OCPP20VariableManager } from './OCPP20VariableManager.js'
import { getVariableMetadata, VARIABLE_REGISTRY } from './OCPP20VariableRegistry.js'
OCPP20IncomingRequestCommand.GET_VARIABLES,
this.handleRequestGetVariables.bind(this) as unknown as IncomingRequestHandler,
],
+ [
+ OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION,
+ this.handleRequestRequestStartTransaction.bind(this) as unknown as IncomingRequestHandler,
+ ],
[
OCPP20IncomingRequestCommand.RESET,
this.handleRequestReset.bind(this) as unknown as IncomingRequestHandler,
}
// 4. EVSE and connector information
- if (chargingStation.evses.size > 0) {
+ if (chargingStation.hasEvses) {
for (const [evseId, evse] of chargingStation.evses) {
reportData.push({
component: {
}
}
}
- } else if (chargingStation.connectors.size > 0) {
+ } else {
// Fallback to connectors if no EVSE structure
for (const [connectorId, connector] of chargingStation.connectors) {
if (connectorId > 0) {
variableCharacteristics: { dataType: DataEnumType.string, supportsMonitoring: true },
})
- if (chargingStation.evses.size > 0) {
+ if (chargingStation.hasEvses) {
for (const [evseId, evse] of chargingStation.evses) {
reportData.push({
component: {
variableCharacteristics: { dataType: DataEnumType.string, supportsMonitoring: true },
})
}
- } else if (chargingStation.connectors.size > 0) {
+ } else {
// Fallback to connectors if no EVSE structure
for (const [connectorId, connector] of chargingStation.connectors) {
if (connectorId > 0) {
}
}
+ private async handleRequestRequestStartTransaction (
+ chargingStation: ChargingStation,
+ commandPayload: OCPP20RequestStartTransactionRequest
+ ): Promise<OCPP20RequestStartTransactionResponse> {
+ const { chargingProfile, evseId, groupIdToken, idToken, remoteStartId } = commandPayload
+ logger.info(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Remote start transaction request received on EVSE ${evseId?.toString() ?? 'undefined'} with idToken ${idToken.idToken} and remoteStartId ${remoteStartId.toString()}`
+ )
+
+ // Validate that EVSE ID is provided
+ if (evseId == null) {
+ const errorMsg = 'EVSE ID is required for RequestStartTransaction'
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: ${errorMsg}`
+ )
+ throw new OCPPError(
+ ErrorType.PROPERTY_CONSTRAINT_VIOLATION,
+ errorMsg,
+ OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION,
+ commandPayload
+ )
+ }
+
+ // Get the first connector for this EVSE
+ const evse = chargingStation.evses.get(evseId)
+ if (evse == null) {
+ const errorMsg = `EVSE ${String(evseId)} not found on charging station`
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: ${errorMsg}`
+ )
+ throw new OCPPError(
+ ErrorType.PROPERTY_CONSTRAINT_VIOLATION,
+ errorMsg,
+ OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION,
+ commandPayload
+ )
+ }
+ const connectorId: number | undefined = evse.connectors.keys().next().value
+ const connectorStatus =
+ connectorId != null ? chargingStation.getConnectorStatus(connectorId) : null
+
+ if (connectorStatus == null || connectorId == null) {
+ const errorMsg = `Connector ${connectorId?.toString() ?? 'undefined'} status is undefined`
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: ${errorMsg}`
+ )
+ throw new OCPPError(
+ ErrorType.INTERNAL_ERROR,
+ errorMsg,
+ OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION,
+ commandPayload
+ )
+ }
+
+ // Check if connector is available for a new transaction
+ if (connectorStatus.transactionStarted === true) {
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Connector ${connectorId.toString()} already has an active transaction`
+ )
+ return {
+ status: RequestStartStopStatusEnumType.Rejected,
+ transactionId: generateUUID(),
+ }
+ }
+
+ // Authorize idToken
+ let isAuthorized = false
+ try {
+ isAuthorized = await this.isIdTokenAuthorized(chargingStation, idToken)
+ } catch (error) {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Authorization error for ${idToken.idToken}:`,
+ error
+ )
+ return {
+ status: RequestStartStopStatusEnumType.Rejected,
+ transactionId: generateUUID(),
+ }
+ }
+
+ if (!isAuthorized) {
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: IdToken ${idToken.idToken} is not authorized`
+ )
+ return {
+ status: RequestStartStopStatusEnumType.Rejected,
+ transactionId: generateUUID(),
+ }
+ }
+
+ // Authorize groupIdToken if provided
+ if (groupIdToken != null) {
+ let isGroupAuthorized = false
+ try {
+ isGroupAuthorized = await this.isIdTokenAuthorized(chargingStation, groupIdToken)
+ } catch (error) {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Group authorization error for ${groupIdToken.idToken}:`,
+ error
+ )
+ return {
+ status: RequestStartStopStatusEnumType.Rejected,
+ transactionId: generateUUID(),
+ }
+ }
+
+ if (!isGroupAuthorized) {
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: GroupIdToken ${groupIdToken.idToken} is not authorized`
+ )
+ return {
+ status: RequestStartStopStatusEnumType.Rejected,
+ transactionId: generateUUID(),
+ }
+ }
+ }
+
+ // Validate charging profile if provided
+ if (chargingProfile != null) {
+ let isValidProfile = false
+ try {
+ isValidProfile = this.validateChargingProfile(chargingStation, chargingProfile, evseId)
+ } catch (error) {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Charging profile validation error:`,
+ error
+ )
+ return {
+ status: RequestStartStopStatusEnumType.Rejected,
+ transactionId: generateUUID(),
+ }
+ }
+
+ if (!isValidProfile) {
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Invalid charging profile`
+ )
+ return {
+ status: RequestStartStopStatusEnumType.Rejected,
+ transactionId: generateUUID(),
+ }
+ }
+ }
+
+ const transactionId = generateUUID()
+
+ // Backup current connector state in case we need to rollback
+ const connectorBackup = {
+ remoteStartId: connectorStatus.remoteStartId,
+ status: connectorStatus.status,
+ transactionEnergyActiveImportRegisterValue:
+ connectorStatus.transactionEnergyActiveImportRegisterValue,
+ transactionId: connectorStatus.transactionId,
+ transactionIdTag: connectorStatus.transactionIdTag,
+ transactionStart: connectorStatus.transactionStart,
+ transactionStarted: connectorStatus.transactionStarted,
+ }
+
+ try {
+ // Set connector transaction state
+ connectorStatus.transactionStarted = true
+ connectorStatus.transactionId = transactionId
+ connectorStatus.transactionIdTag = idToken.idToken
+ connectorStatus.transactionStart = new Date()
+ connectorStatus.transactionEnergyActiveImportRegisterValue = 0
+ connectorStatus.remoteStartId = remoteStartId
+
+ // Update connector status to Occupied
+ await sendAndSetConnectorStatus(
+ chargingStation,
+ connectorId,
+ ConnectorStatusEnum.Occupied,
+ evseId
+ )
+
+ // Store charging profile if provided
+ if (chargingProfile != null) {
+ connectorStatus.chargingProfiles ??= []
+ connectorStatus.chargingProfiles.push(chargingProfile)
+ // TODO: Implement charging profile storage
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Charging profile stored for transaction ${transactionId} (TODO: implement profile storage)`
+ )
+ }
+
+ logger.info(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Remote start transaction accepted on EVSE ${evseId.toString()}, connector ${connectorId.toString()} with transaction ID ${transactionId} for idToken ${idToken.idToken}`
+ )
+
+ return {
+ status: RequestStartStopStatusEnumType.Accepted,
+ transactionId,
+ }
+ } catch (error) {
+ // Rollback connector state on error
+ connectorStatus.transactionStarted = connectorBackup.transactionStarted
+ connectorStatus.transactionId = connectorBackup.transactionId
+ connectorStatus.transactionIdTag = connectorBackup.transactionIdTag
+ connectorStatus.transactionStart = connectorBackup.transactionStart
+ connectorStatus.transactionEnergyActiveImportRegisterValue =
+ connectorBackup.transactionEnergyActiveImportRegisterValue
+ connectorStatus.status = connectorBackup.status
+ connectorStatus.remoteStartId = connectorBackup.remoteStartId
+
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Error starting transaction:`,
+ error
+ )
+ return {
+ status: RequestStartStopStatusEnumType.Rejected,
+ transactionId: generateUUID(),
+ }
+ }
+ }
+
private handleRequestReset (
chargingStation: ChargingStation,
commandPayload: OCPP20ResetRequest
}
}
+ // Helper methods for RequestStartTransaction
+ private async isIdTokenAuthorized (
+ chargingStation: ChargingStation,
+ idToken: OCPP20IdTokenType
+ ): Promise<boolean> {
+ // TODO: Implement proper authorization logic
+ // This should check:
+ // 1. Local authorization list if enabled
+ // 2. Remote authorization via AuthorizeRequest if needed
+ // 3. Cache for known tokens
+ // 4. Return false if authorization fails
+
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.isIdTokenAuthorized: Validating idToken ${idToken.idToken} of type ${idToken.type}`
+ )
+
+ // For now, return true to allow development/testing
+ // TODO: Implement actual async authorization logic
+ return await Promise.resolve(true)
+ }
+
private scheduleEvseReset (
chargingStation: ChargingStation,
evseId: number,
this.reportDataCache.delete(requestId)
}
+ private validateChargingProfile (
+ chargingStation: ChargingStation,
+ chargingProfile: OCPP20ChargingProfileType,
+ evseId: number
+ ): boolean {
+ // TODO: Implement proper charging profile validation
+ // This should validate:
+ // 1. Profile structure and required fields
+ // 2. Schedule periods and limits
+ // 3. Compatibility with EVSE capabilities
+ // 4. Time constraints and validity
+
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.validateChargingProfile: Validating charging profile ${String(chargingProfile.id)} for EVSE ${String(evseId)}`
+ )
+
+ // For now, return true to allow development/testing
+ return true
+ }
+
private validatePayload (
chargingStation: ChargingStation,
commandName: OCPP20IncomingRequestCommand,
OCPP20MeasurandEnumType,
OCPP20OptionalVariableName,
OCPP20RequiredVariableName,
+ OCPP20UnitEnumType,
OCPP20VendorVariableName,
PersistenceEnumType,
ReasonCodeEnumType,
+ type VariableName,
} from '../../../types/index.js'
import { Constants, convertToIntOrNaN, has } from '../../../utils/index.js'
rebootRequired?: boolean
supportedAttributes: AttributeEnumType[]
supportsTarget?: boolean
- unit?: string
+ unit?: OCPP20UnitEnumType
urlSchemes?: string[]
- variable: string
+ variable: VariableName
vendorSpecific?: boolean
}
mutability: MutabilityEnumType.ReadWrite,
persistence: PersistenceEnumType.Persistent,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 's',
+ unit: OCPP20UnitEnumType.SECONDS,
variable: 'Interval',
},
[buildRegistryKey(OCPP20ComponentName.AlignedDataCtrlr as string, 'Measurands')]: {
mutability: MutabilityEnumType.ReadWrite,
persistence: PersistenceEnumType.Persistent,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 's',
+ unit: OCPP20UnitEnumType.SECONDS,
variable: 'TxEndedInterval',
},
[buildRegistryKey(OCPP20ComponentName.AlignedDataCtrlr as string, 'TxEndedMeasurands')]: {
mutability: MutabilityEnumType.ReadWrite,
persistence: PersistenceEnumType.Persistent,
supportedAttributes: [AttributeEnumType.Actual],
- variable: 'TxEndedMeasurands',
+ variable: OCPP20RequiredVariableName.TxEndedMeasurands,
},
// AuthCacheCtrlr Component
mutability: MutabilityEnumType.ReadOnly,
persistence: PersistenceEnumType.Volatile,
supportedAttributes: [AttributeEnumType.Actual, AttributeEnumType.MaxSet],
- unit: 'B',
+ unit: OCPP20UnitEnumType.BYTES,
variable: 'Storage',
},
mutability: MutabilityEnumType.ReadOnly,
persistence: PersistenceEnumType.Persistent,
supportedAttributes: [AttributeEnumType.Actual],
- variable: 'Model',
+ variable: OCPP20DeviceInfoVariableName.Model,
},
[buildRegistryKey(OCPP20ComponentName.ChargingStation as string, 'SupplyPhases')]: {
component: OCPP20ComponentName.ChargingStation as string,
mutability: MutabilityEnumType.ReadOnly,
persistence: PersistenceEnumType.Persistent,
supportedAttributes: [AttributeEnumType.Actual],
- variable: 'VendorName',
+ variable: OCPP20DeviceInfoVariableName.VendorName,
},
[buildRegistryKey(
OCPP20ComponentName.ChargingStation as string,
mutability: MutabilityEnumType.ReadWrite,
persistence: PersistenceEnumType.Persistent,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 's',
+ unit: OCPP20UnitEnumType.SECONDS,
variable: OCPP20OptionalVariableName.WebSocketPingInterval as string,
},
[buildRegistryKey(
persistence: PersistenceEnumType.Persistent,
positive: true,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 'chars',
+ unit: OCPP20UnitEnumType.CHARS,
variable: OCPP20RequiredVariableName.ConfigurationValueSize as string,
},
[buildRegistryKey(
persistence: PersistenceEnumType.Persistent,
positive: true,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 'chars',
+ unit: OCPP20UnitEnumType.CHARS,
variable: OCPP20RequiredVariableName.ReportingValueSize as string,
},
[buildRegistryKey(
persistence: PersistenceEnumType.Persistent,
positive: true,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 'chars',
+ unit: OCPP20UnitEnumType.CHARS,
variable: OCPP20RequiredVariableName.ValueSize as string,
},
mutability: MutabilityEnumType.ReadOnly,
persistence: PersistenceEnumType.Volatile,
supportedAttributes: [AttributeEnumType.Actual],
- variable: 'AvailabilityState',
+ variable: OCPP20DeviceInfoVariableName.AvailabilityState,
},
[buildRegistryKey(OCPP20ComponentName.EVSE as string, 'Available')]: {
component: OCPP20ComponentName.EVSE as string,
persistence: PersistenceEnumType.Volatile,
supportedAttributes: [AttributeEnumType.Actual, AttributeEnumType.MaxSet],
supportsTarget: false,
- unit: 'W',
+ unit: OCPP20UnitEnumType.WATT,
variable: 'Power',
},
[buildRegistryKey(OCPP20ComponentName.EVSE as string, 'SupplyPhases')]: {
mutability: MutabilityEnumType.ReadWrite,
persistence: PersistenceEnumType.Persistent,
supportedAttributes: [AttributeEnumType.Actual],
- variable: 'OrganizationName',
+ variable: OCPP20RequiredVariableName.OrganizationName,
},
[buildRegistryKey(OCPP20ComponentName.ISO15118Ctrlr as string, 'PnCEnabled')]: {
component: OCPP20ComponentName.ISO15118Ctrlr as string,
mutability: MutabilityEnumType.ReadOnly,
persistence: PersistenceEnumType.Persistent,
supportedAttributes: [AttributeEnumType.Actual],
- variable: 'BytesPerMessage',
+ variable: OCPP20RequiredVariableName.BytesPerMessage,
},
[buildRegistryKey(OCPP20ComponentName.LocalAuthListCtrlr as string, 'DisablePostAuthorize')]: {
component: OCPP20ComponentName.LocalAuthListCtrlr as string,
mutability: MutabilityEnumType.ReadOnly,
persistence: PersistenceEnumType.Persistent,
supportedAttributes: [AttributeEnumType.Actual],
- variable: 'ItemsPerMessage',
+ variable: OCPP20RequiredVariableName.ItemsPerMessage,
},
[buildRegistryKey(OCPP20ComponentName.LocalAuthListCtrlr as string, 'Storage')]: {
characteristics: {
mutability: MutabilityEnumType.ReadOnly,
persistence: PersistenceEnumType.Volatile,
supportedAttributes: [AttributeEnumType.Actual, AttributeEnumType.MaxSet],
- unit: 'B',
+ unit: OCPP20UnitEnumType.BYTES,
variable: 'Storage',
},
mutability: MutabilityEnumType.ReadOnly,
persistence: PersistenceEnumType.Persistent,
supportedAttributes: [AttributeEnumType.Actual],
- variable: 'BytesPerMessage',
+ variable: OCPP20RequiredVariableName.BytesPerMessage,
},
[buildRegistryKey(
OCPP20ComponentName.MonitoringCtrlr as string,
mutability: MutabilityEnumType.ReadOnly,
persistence: PersistenceEnumType.Persistent,
supportedAttributes: [AttributeEnumType.Actual],
- variable: 'BytesPerMessage',
+ variable: OCPP20RequiredVariableName.BytesPerMessage,
},
[buildRegistryKey(OCPP20ComponentName.MonitoringCtrlr as string, 'Enabled')]: {
component: OCPP20ComponentName.MonitoringCtrlr as string,
mutability: MutabilityEnumType.ReadOnly,
persistence: PersistenceEnumType.Persistent,
supportedAttributes: [AttributeEnumType.Actual],
- variable: 'ItemsPerMessage',
+ variable: OCPP20RequiredVariableName.ItemsPerMessage,
},
[buildRegistryKey(
OCPP20ComponentName.MonitoringCtrlr as string,
mutability: MutabilityEnumType.ReadOnly,
persistence: PersistenceEnumType.Persistent,
supportedAttributes: [AttributeEnumType.Actual],
- variable: 'ItemsPerMessage',
+ variable: OCPP20RequiredVariableName.ItemsPerMessage,
},
[buildRegistryKey(OCPP20ComponentName.MonitoringCtrlr as string, 'MonitoringBase')]: {
component: OCPP20ComponentName.MonitoringCtrlr as string,
persistence: PersistenceEnumType.Persistent,
positive: true,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 's',
+ unit: OCPP20UnitEnumType.SECONDS,
variable: OCPP20OptionalVariableName.HeartbeatInterval as string,
},
[buildRegistryKey(
mutability: MutabilityEnumType.ReadWrite,
persistence: PersistenceEnumType.Persistent,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 's',
+ unit: OCPP20UnitEnumType.SECONDS,
variable: OCPP20OptionalVariableName.WebSocketPingInterval as string,
},
[buildRegistryKey(
persistence: PersistenceEnumType.Persistent,
positive: true,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 's',
+ unit: OCPP20UnitEnumType.SECONDS,
variable: OCPP20RequiredVariableName.MessageAttemptInterval as string,
},
[buildRegistryKey(
persistence: PersistenceEnumType.Persistent,
positive: true,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 's',
+ unit: OCPP20UnitEnumType.SECONDS,
variable: OCPP20RequiredVariableName.MessageTimeout as string,
},
[buildRegistryKey(
persistence: PersistenceEnumType.Persistent,
positive: true,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 's',
+ unit: OCPP20UnitEnumType.SECONDS,
variable: OCPP20RequiredVariableName.OfflineThreshold as string,
},
[buildRegistryKey(
mutability: MutabilityEnumType.ReadWrite,
persistence: PersistenceEnumType.Persistent,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 's',
+ unit: OCPP20UnitEnumType.SECONDS,
variable: 'TxEndedInterval',
},
[buildRegistryKey(
mutability: MutabilityEnumType.ReadOnly,
persistence: PersistenceEnumType.Volatile,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 'A',
+ unit: OCPP20UnitEnumType.AMP,
variable: OCPP20MeasurandEnumType.CURRENT_IMPORT,
},
[buildRegistryKey(
mutability: MutabilityEnumType.ReadOnly,
persistence: PersistenceEnumType.Volatile,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 'Wh',
+ unit: OCPP20UnitEnumType.WATT_HOUR,
variable: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
},
[buildRegistryKey(
mutability: MutabilityEnumType.ReadOnly,
persistence: PersistenceEnumType.Volatile,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 'W',
+ unit: OCPP20UnitEnumType.WATT,
variable: OCPP20MeasurandEnumType.POWER_ACTIVE_IMPORT,
},
[buildRegistryKey(
mutability: MutabilityEnumType.ReadOnly,
persistence: PersistenceEnumType.Volatile,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 'V',
+ unit: OCPP20UnitEnumType.VOLT,
variable: OCPP20MeasurandEnumType.VOLTAGE,
},
[buildRegistryKey(
persistence: PersistenceEnumType.Volatile,
positive: true,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 's',
+ unit: OCPP20UnitEnumType.SECONDS,
variable: OCPP20RequiredVariableName.TxUpdatedInterval as string,
},
[buildRegistryKey(
mutability: MutabilityEnumType.ReadWrite,
persistence: PersistenceEnumType.Persistent,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 's',
+ unit: OCPP20UnitEnumType.SECONDS,
variable: 'CertSigningWaitMinimum',
},
[buildRegistryKey(OCPP20ComponentName.SecurityCtrlr as string, 'Identity')]: {
mutability: MutabilityEnumType.ReadWrite,
persistence: PersistenceEnumType.Persistent,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 'Percent',
+ unit: OCPP20UnitEnumType.PERCENT,
variable: 'LimitChangeSignificance',
},
[buildRegistryKey(
mutability: MutabilityEnumType.ReadOnly,
persistence: PersistenceEnumType.Volatile,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 's',
+ unit: OCPP20UnitEnumType.SECONDS,
variable: 'ChargingTime',
},
[buildRegistryKey(OCPP20ComponentName.TxCtrlr as string, 'MaxEnergyOnInvalidId')]: {
mutability: MutabilityEnumType.ReadWrite,
persistence: PersistenceEnumType.Persistent,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 'Wh',
+ unit: OCPP20UnitEnumType.WATT_HOUR,
variable: 'MaxEnergyOnInvalidId',
},
[buildRegistryKey(OCPP20ComponentName.TxCtrlr as string, 'TxBeforeAcceptedEnabled')]: {
persistence: PersistenceEnumType.Persistent,
positive: true,
supportedAttributes: [AttributeEnumType.Actual],
- unit: 's',
+ unit: OCPP20UnitEnumType.SECONDS,
variable: OCPP20RequiredVariableName.EVConnectionTimeOut as string,
},
[buildRegistryKey(
MeterValuePhase,
MeterValueUnit,
type OCPP16ChargePointStatus,
+ type OCPP16SampledValue,
type OCPP16StatusNotificationRequest,
type OCPP20ConnectorStatusEnumType,
+ type OCPP20MeterValue,
+ type OCPP20SampledValue,
type OCPP20StatusNotificationRequest,
OCPPVersion,
RequestCommand,
timestamp: new Date(),
} satisfies OCPP20StatusNotificationRequest
default:
- throw new BaseError('Cannot build status notification payload: OCPP version not supported')
+ throw new OCPPError(
+ ErrorType.INTERNAL_ERROR,
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ `Cannot build status notification payload: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`,
+ RequestCommand.STATUS_NOTIFICATION
+ )
}
}
}
break
default:
- throw new BaseError(
+ throw new OCPPError(
+ ErrorType.INTERNAL_ERROR,
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`
+ `Cannot check connector status transition: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`,
+ RequestCommand.STATUS_NOTIFICATION
)
}
if (!transitionAllowed) {
)
: randomInt(socMinimumValue, socMaximumValue + 1)
meterValue.sampledValue.push(
- buildSampledValue(socSampledValueTemplate, socSampledValueTemplateValue)
+ buildSampledValue(
+ chargingStation.stationInfo.ocppVersion,
+ socSampledValueTemplate,
+ socSampledValueTemplateValue
+ )
)
const sampledValuesIndex = meterValue.sampledValue.length - 1
if (
meterValue.sampledValue[sampledValuesIndex].measurand ??
MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- }: connector id ${connectorId.toString()}, transaction id ${connector?.transactionId?.toString()}, value: ${socMinimumValue.toString()}/${
- meterValue.sampledValue[sampledValuesIndex].value
- }/${socMaximumValue.toString()}`
+ }: connector id ${connectorId.toString()}, transaction id ${connector?.transactionId?.toString()}, value: ${socMinimumValue.toString()}/${meterValue.sampledValue[
+ sampledValuesIndex
+ ].value.toString()}/${socMaximumValue.toString()}`
)
}
}
chargingStation.stationInfo.mainVoltageMeterValues === true)
) {
meterValue.sampledValue.push(
- buildSampledValue(voltageSampledValueTemplate, voltageMeasurandValue)
+ buildSampledValue(
+ chargingStation.stationInfo.ocppVersion,
+ voltageSampledValueTemplate,
+ voltageMeasurandValue
+ )
)
}
for (
}
meterValue.sampledValue.push(
buildSampledValue(
+ chargingStation.stationInfo.ocppVersion,
voltagePhaseLineToNeutralSampledValueTemplate ?? voltageSampledValueTemplate,
voltagePhaseLineToNeutralMeasurandValue ?? voltageMeasurandValue,
undefined,
)
meterValue.sampledValue.push(
buildSampledValue(
+ chargingStation.stationInfo.ocppVersion,
voltagePhaseLineToLineSampledValueTemplate ?? voltageSampledValueTemplate,
voltagePhaseLineToLineMeasurandValue ?? defaultVoltagePhaseLineToLineMeasurandValue,
undefined,
throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES)
}
meterValue.sampledValue.push(
- buildSampledValue(powerSampledValueTemplate, powerMeasurandValues.allPhases)
+ buildSampledValue(
+ chargingStation.stationInfo.ocppVersion,
+ powerSampledValueTemplate,
+ powerMeasurandValues.allPhases
+ )
)
const sampledValuesIndex = meterValue.sampledValue.length - 1
const connectorMaximumPowerRounded = roundTo(connectorMaximumPower / unitDivider, 2)
meterValue.sampledValue[sampledValuesIndex].measurand ??
MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- }: connector id ${connectorId.toString()}, transaction id ${connector?.transactionId?.toString()}, value: ${connectorMinimumPowerRounded.toString()}/${
- meterValue.sampledValue[sampledValuesIndex].value
- }/${connectorMaximumPowerRounded.toString()}`
+ }: connector id ${connectorId.toString()}, transaction id ${connector?.transactionId?.toString()}, value: ${connectorMinimumPowerRounded.toString()}/${meterValue.sampledValue[
+ sampledValuesIndex
+ ].value.toString()}/${connectorMaximumPowerRounded.toString()}`
)
}
for (
const phaseValue = `L${phase.toString()}-N`
meterValue.sampledValue.push(
buildSampledValue(
+ chargingStation.stationInfo.ocppVersion,
powerPerPhaseSampledValueTemplates[
`L${phase.toString()}` as keyof MeasurandPerPhaseSampledValueTemplates
] ?? powerSampledValueTemplate,
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- }, connector id ${connectorId.toString()}, transaction id ${connector?.transactionId?.toString()}, value: ${connectorMinimumPowerPerPhaseRounded.toString()}/${
- meterValue.sampledValue[sampledValuesPerPhaseIndex].value
- }/${connectorMaximumPowerPerPhaseRounded.toString()}`
+ }, connector id ${connectorId.toString()}, transaction id ${connector?.transactionId?.toString()}, value: ${connectorMinimumPowerPerPhaseRounded.toString()}/${meterValue.sampledValue[
+ sampledValuesPerPhaseIndex
+ ].value.toString()}/${connectorMaximumPowerPerPhaseRounded.toString()}`
)
}
}
throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES)
}
meterValue.sampledValue.push(
- buildSampledValue(currentSampledValueTemplate, currentMeasurandValues.allPhases)
+ buildSampledValue(
+ chargingStation.stationInfo.ocppVersion,
+ currentSampledValueTemplate,
+ currentMeasurandValues.allPhases
+ )
)
const sampledValuesIndex = meterValue.sampledValue.length - 1
if (
meterValue.sampledValue[sampledValuesIndex].measurand ??
MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- }: connector id ${connectorId.toString()}, transaction id ${connector?.transactionId?.toString()}, value: ${connectorMinimumAmperage.toString()}/${
- meterValue.sampledValue[sampledValuesIndex].value
- }/${connectorMaximumAmperage.toString()}`
+ }: connector id ${connectorId.toString()}, transaction id ${connector?.transactionId?.toString()}, value: ${connectorMinimumAmperage.toString()}/${meterValue.sampledValue[
+ sampledValuesIndex
+ ].value.toString()}/${connectorMaximumAmperage.toString()}`
)
}
for (
const phaseValue = `L${phase.toString()}`
meterValue.sampledValue.push(
buildSampledValue(
+ chargingStation.stationInfo.ocppVersion,
currentPerPhaseSampledValueTemplates[
phaseValue as keyof MeasurandPerPhaseSampledValueTemplates
] ?? currentSampledValueTemplate,
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- }, connector id ${connectorId.toString()}, transaction id ${connector?.transactionId?.toString()}, value: ${connectorMinimumAmperage.toString()}/${
- meterValue.sampledValue[sampledValuesPerPhaseIndex].value
- }/${connectorMaximumAmperage.toString()}`
+ }, connector id ${connectorId.toString()}, transaction id ${connector?.transactionId?.toString()}, value: ${connectorMinimumAmperage.toString()}/${meterValue.sampledValue[
+ sampledValuesPerPhaseIndex
+ ].value.toString()}/${connectorMaximumAmperage.toString()}`
)
}
}
energySampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT
)
: getRandomFloatRounded(connectorMaximumEnergyRounded, connectorMinimumEnergyRounded)
- // Persist previous value on connector
if (connector != null) {
if (
connector.energyActiveImportRegisterValue != null &&
}
meterValue.sampledValue.push(
buildSampledValue(
+ chargingStation.stationInfo.ocppVersion,
energySampledValueTemplate,
roundTo(
chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) /
}
return meterValue
case OCPPVersion.VERSION_20:
- case OCPPVersion.VERSION_201:
+ case OCPPVersion.VERSION_201: {
+ const meterValue: OCPP20MeterValue = {
+ sampledValue: [],
+ timestamp: new Date(),
+ }
+ // SoC measurand
+ socSampledValueTemplate = getSampledValueTemplate(
+ chargingStation,
+ connectorId,
+ MeterValueMeasurand.STATE_OF_CHARGE
+ )
+ if (socSampledValueTemplate != null) {
+ const socMaximumValue = 100
+ const socMinimumValue = socSampledValueTemplate.minimumValue ?? 0
+ const socSampledValueTemplateValue = isNotEmptyString(socSampledValueTemplate.value)
+ ? getRandomFloatFluctuatedRounded(
+ Number.parseInt(socSampledValueTemplate.value),
+ socSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT
+ )
+ : randomInt(socMinimumValue, socMaximumValue + 1)
+ meterValue.sampledValue.push(
+ buildSampledValue(
+ chargingStation.stationInfo.ocppVersion,
+ socSampledValueTemplate,
+ socSampledValueTemplateValue
+ )
+ )
+ const sampledValuesIndex = meterValue.sampledValue.length - 1
+ if (
+ convertToInt(meterValue.sampledValue[sampledValuesIndex].value) > socMaximumValue ||
+ convertToInt(meterValue.sampledValue[sampledValuesIndex].value) < socMinimumValue ||
+ debug
+ ) {
+ logger.error(
+ `${chargingStation.logPrefix()} MeterValues measurand ${
+ meterValue.sampledValue[sampledValuesIndex].measurand ??
+ MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ }: connector id ${connectorId.toString()}, transaction id ${connector?.transactionId?.toString()}, value: ${socMinimumValue.toString()}/${meterValue.sampledValue[
+ sampledValuesIndex
+ ].value.toString()}/${socMaximumValue.toString()}`
+ )
+ }
+ }
+ // Voltage measurand
+ voltageSampledValueTemplate = getSampledValueTemplate(
+ chargingStation,
+ connectorId,
+ MeterValueMeasurand.VOLTAGE
+ )
+ if (voltageSampledValueTemplate != null) {
+ const voltageSampledValueTemplateValue = isNotEmptyString(voltageSampledValueTemplate.value)
+ ? Number.parseInt(voltageSampledValueTemplate.value)
+ : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ chargingStation.stationInfo.voltageOut!
+ const fluctuationPercent =
+ voltageSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT
+ const voltageMeasurandValue = getRandomFloatFluctuatedRounded(
+ voltageSampledValueTemplateValue,
+ fluctuationPercent
+ )
+ if (
+ chargingStation.getNumberOfPhases() !== 3 ||
+ (chargingStation.getNumberOfPhases() === 3 &&
+ chargingStation.stationInfo.mainVoltageMeterValues === true)
+ ) {
+ meterValue.sampledValue.push(
+ buildSampledValue(
+ chargingStation.stationInfo.ocppVersion,
+ voltageSampledValueTemplate,
+ voltageMeasurandValue
+ )
+ )
+ }
+ for (
+ let phase = 1;
+ chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
+ phase++
+ ) {
+ const phaseLineToNeutralValue = `L${phase.toString()}-N`
+ const voltagePhaseLineToNeutralSampledValueTemplate = getSampledValueTemplate(
+ chargingStation,
+ connectorId,
+ MeterValueMeasurand.VOLTAGE,
+ phaseLineToNeutralValue as MeterValuePhase
+ )
+ let voltagePhaseLineToNeutralMeasurandValue: number | undefined
+ if (voltagePhaseLineToNeutralSampledValueTemplate != null) {
+ const voltagePhaseLineToNeutralSampledValueTemplateValue = isNotEmptyString(
+ voltagePhaseLineToNeutralSampledValueTemplate.value
+ )
+ ? Number.parseInt(voltagePhaseLineToNeutralSampledValueTemplate.value)
+ : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ chargingStation.stationInfo.voltageOut!
+ const fluctuationPhaseToNeutralPercent =
+ voltagePhaseLineToNeutralSampledValueTemplate.fluctuationPercent ??
+ Constants.DEFAULT_FLUCTUATION_PERCENT
+ voltagePhaseLineToNeutralMeasurandValue = getRandomFloatFluctuatedRounded(
+ voltagePhaseLineToNeutralSampledValueTemplateValue,
+ fluctuationPhaseToNeutralPercent
+ )
+ }
+ meterValue.sampledValue.push(
+ buildSampledValue(
+ chargingStation.stationInfo.ocppVersion,
+ voltagePhaseLineToNeutralSampledValueTemplate ?? voltageSampledValueTemplate,
+ voltagePhaseLineToNeutralMeasurandValue ?? voltageMeasurandValue,
+ undefined,
+ phaseLineToNeutralValue as MeterValuePhase
+ )
+ )
+ if (chargingStation.stationInfo.phaseLineToLineVoltageMeterValues === true) {
+ const phaseLineToLineValue = `L${phase.toString()}-L${
+ (phase + 1) % chargingStation.getNumberOfPhases() !== 0
+ ? ((phase + 1) % chargingStation.getNumberOfPhases()).toString()
+ : chargingStation.getNumberOfPhases().toString()
+ }`
+ const voltagePhaseLineToLineValueRounded = roundTo(
+ Math.sqrt(chargingStation.getNumberOfPhases()) *
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ chargingStation.stationInfo.voltageOut!,
+ 2
+ )
+ const voltagePhaseLineToLineSampledValueTemplate = getSampledValueTemplate(
+ chargingStation,
+ connectorId,
+ MeterValueMeasurand.VOLTAGE,
+ phaseLineToLineValue as MeterValuePhase
+ )
+ let voltagePhaseLineToLineMeasurandValue: number | undefined
+ if (voltagePhaseLineToLineSampledValueTemplate != null) {
+ const voltagePhaseLineToLineSampledValueTemplateValue = isNotEmptyString(
+ voltagePhaseLineToLineSampledValueTemplate.value
+ )
+ ? Number.parseInt(voltagePhaseLineToLineSampledValueTemplate.value)
+ : voltagePhaseLineToLineValueRounded
+ const fluctuationPhaseLineToLinePercent =
+ voltagePhaseLineToLineSampledValueTemplate.fluctuationPercent ??
+ Constants.DEFAULT_FLUCTUATION_PERCENT
+ voltagePhaseLineToLineMeasurandValue = getRandomFloatFluctuatedRounded(
+ voltagePhaseLineToLineSampledValueTemplateValue,
+ fluctuationPhaseLineToLinePercent
+ )
+ }
+ const defaultVoltagePhaseLineToLineMeasurandValue = getRandomFloatFluctuatedRounded(
+ voltagePhaseLineToLineValueRounded,
+ fluctuationPercent
+ )
+ meterValue.sampledValue.push(
+ buildSampledValue(
+ chargingStation.stationInfo.ocppVersion,
+ voltagePhaseLineToLineSampledValueTemplate ?? voltageSampledValueTemplate,
+ voltagePhaseLineToLineMeasurandValue ?? defaultVoltagePhaseLineToLineMeasurandValue,
+ undefined,
+ phaseLineToLineValue as MeterValuePhase
+ )
+ )
+ }
+ }
+ }
+ // Energy.Active.Import.Register measurand
+ energySampledValueTemplate = getSampledValueTemplate(chargingStation, connectorId)
+ if (energySampledValueTemplate != null) {
+ checkMeasurandPowerDivider(chargingStation, energySampledValueTemplate.measurand)
+ const unitDivider =
+ energySampledValueTemplate.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1
+ connectorMaximumAvailablePower == null &&
+ (connectorMaximumAvailablePower =
+ chargingStation.getConnectorMaximumAvailablePower(connectorId))
+ const connectorMaximumEnergyRounded = roundTo(
+ (connectorMaximumAvailablePower * interval) / (3600 * 1000),
+ 2
+ )
+ const connectorMinimumEnergyRounded = roundTo(
+ energySampledValueTemplate.minimumValue ?? 0,
+ 2
+ )
+ const energyValueRounded = isNotEmptyString(energySampledValueTemplate.value)
+ ? getRandomFloatFluctuatedRounded(
+ getLimitFromSampledValueTemplateCustomValue(
+ energySampledValueTemplate.value,
+ connectorMaximumEnergyRounded,
+ connectorMinimumEnergyRounded,
+ {
+ fallbackValue: connectorMinimumEnergyRounded,
+ limitationEnabled: chargingStation.stationInfo.customValueLimitationMeterValues,
+ unitMultiplier: unitDivider,
+ }
+ ),
+ energySampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT
+ )
+ : getRandomFloatRounded(connectorMaximumEnergyRounded, connectorMinimumEnergyRounded)
+ if (connector != null) {
+ if (
+ connector.energyActiveImportRegisterValue != null &&
+ connector.energyActiveImportRegisterValue >= 0 &&
+ connector.transactionEnergyActiveImportRegisterValue != null &&
+ connector.transactionEnergyActiveImportRegisterValue >= 0
+ ) {
+ connector.energyActiveImportRegisterValue += energyValueRounded
+ connector.transactionEnergyActiveImportRegisterValue += energyValueRounded
+ } else {
+ connector.energyActiveImportRegisterValue = 0
+ connector.transactionEnergyActiveImportRegisterValue = 0
+ }
+ }
+ meterValue.sampledValue.push(
+ buildSampledValue(
+ chargingStation.stationInfo.ocppVersion,
+ energySampledValueTemplate,
+ roundTo(
+ chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) /
+ unitDivider,
+ 2
+ )
+ )
+ )
+ const sampledValuesIndex = meterValue.sampledValue.length - 1
+ if (
+ energyValueRounded > connectorMaximumEnergyRounded ||
+ energyValueRounded < connectorMinimumEnergyRounded ||
+ debug
+ ) {
+ logger.error(
+ `${chargingStation.logPrefix()} MeterValues measurand ${
+ meterValue.sampledValue[sampledValuesIndex].measurand ??
+ MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ }: connector id ${connectorId.toString()}, transaction id ${connector?.transactionId?.toString()}, value: ${connectorMinimumEnergyRounded.toString()}/${energyValueRounded.toString()}/${connectorMaximumEnergyRounded.toString()}, duration: ${interval.toString()}ms`
+ )
+ }
+ }
+ return meterValue
+ }
default:
- throw new BaseError(
+ throw new OCPPError(
+ ErrorType.INTERNAL_ERROR,
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- `Cannot build meterValue: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`
+ `Cannot build meterValue: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`,
+ RequestCommand.METER_VALUES
)
}
}
let unitDivider: number
switch (chargingStation.stationInfo?.ocppVersion) {
case OCPPVersion.VERSION_16:
+ case OCPPVersion.VERSION_20:
+ case OCPPVersion.VERSION_201:
meterValue = {
sampledValue: [],
timestamp: new Date(),
unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1
meterValue.sampledValue.push(
buildSampledValue(
+ chargingStation.stationInfo.ocppVersion,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
sampledValueTemplate!,
roundTo((meterStop ?? 0) / unitDivider, 4),
)
)
return meterValue
- case OCPPVersion.VERSION_20:
- case OCPPVersion.VERSION_201:
default:
- throw new BaseError(
+ throw new OCPPError(
+ ErrorType.INTERNAL_ERROR,
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- `Cannot build meterValue: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`
+ `Cannot build meterValue: OCPP version ${chargingStation.stationInfo?.ocppVersion} not supported`,
+ RequestCommand.METER_VALUES
)
}
}
)
}
+const isMeasurandSupported = (measurand: MeterValueMeasurand): boolean => {
+ const supportedMeasurands = OCPPConstants.OCPP_MEASURANDS_SUPPORTED as readonly string[]
+ return supportedMeasurands.includes(measurand as string)
+}
+
const getSampledValueTemplate = (
chargingStation: ChargingStation,
connectorId: number,
phase?: MeterValuePhase
): SampledValueTemplate | undefined => {
const onPhaseStr = phase != null ? `on phase ${phase} ` : ''
- if (!OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(measurand)) {
+ if (!isMeasurandSupported(measurand)) {
logger.warn(
`${chargingStation.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connector id ${connectorId.toString()}`
)
index++
) {
if (
- !OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes(
+ !isMeasurandSupported(
sampledValueTemplates[index].measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
)
) {
)
}
-const buildSampledValue = (
+function buildSampledValue (
+ ocppVersion: OCPPVersion.VERSION_16 | undefined,
+ sampledValueTemplate: SampledValueTemplate,
+ value: number,
+ context?: MeterValueContext,
+ phase?: MeterValuePhase
+): OCPP16SampledValue
+function buildSampledValue (
+ ocppVersion: OCPPVersion.VERSION_20 | OCPPVersion.VERSION_201 | undefined,
sampledValueTemplate: SampledValueTemplate,
value: number,
context?: MeterValueContext,
phase?: MeterValuePhase
-): SampledValue => {
+): OCPP20SampledValue
+/**
+ * Builds a sampled value object according to the specified OCPP version
+ * @param ocppVersion - The OCPP version to use for formatting the sampled value
+ * @param sampledValueTemplate - Template containing measurement configuration and metadata
+ * @param value - The measured numeric value to be included in the sampled value
+ * @param context - Optional context specifying when the measurement was taken (e.g., Sample.Periodic)
+ * @param phase - Optional phase information for multi-phase electrical measurements
+ * @returns A sampled value object formatted according to the specified OCPP version
+ */
+function buildSampledValue (
+ ocppVersion: OCPPVersion | undefined,
+ sampledValueTemplate: SampledValueTemplate,
+ value: number,
+ context?: MeterValueContext,
+ phase?: MeterValuePhase
+): SampledValue {
const sampledValueContext = context ?? sampledValueTemplate.context
const sampledValueLocation =
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- sampledValueTemplate.location ?? getMeasurandDefaultLocation(sampledValueTemplate.measurand!)
+ sampledValueTemplate.location ?? getMeasurandDefaultLocation(sampledValueTemplate.measurand)
const sampledValuePhase = phase ?? sampledValueTemplate.phase
- return {
- ...(sampledValueTemplate.unit != null && {
- unit: sampledValueTemplate.unit,
- }),
- ...(sampledValueContext != null && { context: sampledValueContext }),
- ...(sampledValueTemplate.measurand != null && {
- measurand: sampledValueTemplate.measurand,
- }),
- ...(sampledValueLocation != null && { location: sampledValueLocation }),
- ...{ value: value.toString() },
- ...(sampledValuePhase != null && { phase: sampledValuePhase }),
- } satisfies SampledValue
+
+ switch (ocppVersion) {
+ case OCPPVersion.VERSION_16:
+ // OCPP 1.6 format
+ return {
+ ...(sampledValueTemplate.unit != null && {
+ unit: sampledValueTemplate.unit,
+ }),
+ ...(sampledValueContext != null && { context: sampledValueContext }),
+ ...(sampledValueTemplate.measurand != null && {
+ measurand: sampledValueTemplate.measurand,
+ }),
+ ...(sampledValueLocation != null && { location: sampledValueLocation }),
+ value: value.toString(), // OCPP 1.6 uses string
+ ...(sampledValuePhase != null && { phase: sampledValuePhase }),
+ } as OCPP16SampledValue
+ case OCPPVersion.VERSION_20:
+ case OCPPVersion.VERSION_201:
+ // OCPP 2.0 format
+ return {
+ ...(sampledValueContext != null && { context: sampledValueContext }),
+ ...(sampledValueTemplate.measurand != null && {
+ measurand: sampledValueTemplate.measurand,
+ }),
+ ...(sampledValueLocation != null && { location: sampledValueLocation }),
+ value, // OCPP 2.0 uses number
+ ...(sampledValuePhase != null && { phase: sampledValuePhase }),
+ ...(sampledValueTemplate.unitOfMeasure != null && {
+ unitOfMeasure: sampledValueTemplate.unitOfMeasure,
+ }),
+ } as OCPP20SampledValue
+ default:
+ throw new OCPPError(
+ ErrorType.INTERNAL_ERROR,
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ `Cannot build sampledValue: OCPP version ${ocppVersion} not supported`,
+ RequestCommand.METER_VALUES
+ )
+ }
}
const getMeasurandDefaultLocation = (
idTagLocalAuthorized?: boolean
localAuthorizeIdTag?: string
MeterValues: SampledValueTemplate[]
+ remoteStartId?: number
reservation?: Reservation
status?: ConnectorStatusEnum
transactionBeginMeterValue?: MeterValue
transactionEnergyActiveImportRegisterValue?: number // In Wh
- transactionId?: number
+ transactionId?: number | string
transactionIdTag?: string
transactionRemoteStarted?: boolean
transactionSetInterval?: NodeJS.Timeout
L3?: SampledValueTemplate
}
-export interface SampledValueTemplate extends SampledValue {
+export type SampledValueTemplate = SampledValue & {
fluctuationPercent?: number
minimumValue?: number
}
} from './ocpp/1.6/Transaction.js'
export {
BootReasonEnumType,
- type ComponentType,
type CustomDataType,
DataEnumType,
GenericDeviceModelStatusEnumType,
OCPP20ComponentName,
- OCPP20ConnectorStatusEnumType,
+ OCPP20UnitEnumType,
ReasonCodeEnumType,
ReportBaseEnumType,
- type ReportDataType,
ResetEnumType,
ResetStatusEnumType,
} from './ocpp/2.0/Common.js'
OCPP20IncomingRequestCommand,
type OCPP20NotifyReportRequest,
OCPP20RequestCommand,
+ type OCPP20RequestStartTransactionRequest,
type OCPP20ResetRequest,
type OCPP20SetVariablesRequest,
type OCPP20StatusNotificationRequest,
OCPP20GetVariablesResponse,
OCPP20HeartbeatResponse,
OCPP20NotifyReportResponse,
+ OCPP20RequestStartTransactionResponse,
OCPP20ResetResponse,
OCPP20SetVariablesResponse,
OCPP20StatusNotificationResponse,
} from './ocpp/2.0/Responses.js'
+export {
+ type ComponentType,
+ OCPP20ChargingProfileKindEnumType,
+ OCPP20ChargingProfilePurposeEnumType,
+ type OCPP20ChargingProfileType,
+ OCPP20ChargingRateUnitEnumType,
+ type OCPP20ChargingSchedulePeriodType,
+ type OCPP20ChargingScheduleType,
+ OCPP20ConnectorStatusEnumType,
+ type OCPP20IdTokenType,
+ OCPP20TransactionEventEnumType,
+ type OCPP20TransactionEventRequest,
+ type OCPP20TransactionEventResponse,
+ type OCPP20TransactionType,
+ OCPP20TriggerReasonEnumType,
+ RequestStartStopStatusEnumType,
+} from './ocpp/2.0/Transaction.js'
export {
AttributeEnumType,
GetVariableStatusEnumType,
type OCPP20SetVariableResultType,
OCPP20VendorVariableName,
PersistenceEnumType,
+ type ReportDataType,
SetVariableStatusEnumType,
+ type VariableName,
type VariableType,
} from './ocpp/2.0/Variables.js'
export { ChargePointErrorCode } from './ocpp/ChargePointErrorCode.js'
import type { JsonObject } from '../../JsonType.js'
import type { GenericStatus } from '../Common.js'
-import type { VariableType } from './Variables.js'
export enum BootReasonEnumType {
ApplicationReset = 'ApplicationReset',
VehicleIdSensor = 'VehicleIdSensor',
}
-export enum OCPP20ConnectorEnumType {
- cCCS1 = 'cCCS1',
- cCCS2 = 'cCCS2',
- cG105 = 'cG105',
- cTesla = 'cTesla',
- cType1 = 'cType1',
- cType2 = 'cType2',
- Other1PhMax16A = 'Other1PhMax16A',
- Other1PhOver16A = 'Other1PhOver16A',
- Other3Ph = 'Other3Ph',
- Pan = 'Pan',
- s309_1P_16A = 's309-1P-16A',
- s309_1P_32A = 's309-1P-32A',
- s309_3P_16A = 's309-3P-16A',
- s309_3P_32A = 's309-3P-32A',
- sBS1361 = 'sBS1361',
- sCEE_7_7 = 'sCEE-7-7',
- sType2 = 'sType2',
- sType3 = 'sType3',
- Undetermined = 'Undetermined',
- Unknown = 'Unknown',
- wInductive = 'wInductive',
- wResonant = 'wResonant',
-}
-
-export enum OCPP20ConnectorStatusEnumType {
- Available = 'Available',
- Faulted = 'Faulted',
- Occupied = 'Occupied',
- Reserved = 'Reserved',
- Unavailable = 'Unavailable',
+export enum OCPP20UnitEnumType {
+ AMP = 'A',
+ ARBITRARY_STRENGTH_UNIT = 'ASU',
+ BYTES = 'B',
+ CELSIUS = 'Celsius',
+ CHARS = 'chars', // Custom extension for character count measurements
+ DECIBEL = 'dB',
+ DECIBEL_MILLIWATT = 'dBm', // cspell:ignore MILLIWATT
+ DEGREES = 'Deg',
+ FAHRENHEIT = 'Fahrenheit',
+ HERTZ = 'Hz',
+ KELVIN = 'K',
+ KILO_PASCAL = 'kPa',
+ KILO_VAR = 'kvar',
+ KILO_VAR_HOUR = 'kvarh',
+ KILO_VOLT_AMP = 'kVA',
+ KILO_VOLT_AMP_HOUR = 'kVAh',
+ KILO_WATT = 'kW',
+ KILO_WATT_HOUR = 'kWh',
+ LUX = 'lx',
+ METER = 'm',
+ METER_PER_SECOND_SQUARED = 'ms2',
+ NEWTON = 'N',
+ OHM = 'Ohm',
+ PERCENT = 'Percent',
+ RELATIVE_HUMIDITY = 'RH',
+ REVOLUTIONS_PER_MINUTE = 'RPM',
+ SECONDS = 's',
+ VAR = 'var',
+ VAR_HOUR = 'varh',
+ VOLT = 'V',
+ VOLT_AMP = 'VA',
+ VOLT_AMP_HOUR = 'VAh',
+ WATT = 'W',
+ WATT_HOUR = 'Wh',
}
export enum OperationalStatusEnumType {
vendorName: string
}
-export interface ComponentType extends JsonObject {
- evse?: EVSEType
- instance?: string
- name: OCPP20ComponentName | string
-}
-
export interface CustomDataType extends JsonObject {
vendorId: string
}
export type GenericStatusEnumType = GenericStatus
+export interface ModemType extends JsonObject {
+ customData?: CustomDataType
+ iccid?: string
+ imsi?: string
+}
+
export interface OCSPRequestDataType extends JsonObject {
hashAlgorithm: HashAlgorithmEnumType
issuerKeyHash: string
serialNumber: string
}
-export interface ReportDataType extends JsonObject {
- component: ComponentType
- variable: VariableType
- variableAttribute?: VariableAttributeType[]
- variableCharacteristics?: VariableCharacteristicsType
-}
-
export interface StatusInfoType extends JsonObject {
additionalInfo?: string
customData?: CustomDataType
reasonCode: ReasonCodeEnumType
}
-
-interface EVSEType extends JsonObject {
- connectorId?: number
- id: number
-}
-
-interface ModemType extends JsonObject {
- customData?: CustomDataType
- iccid?: string
- imsi?: string
-}
-
-interface VariableAttributeType extends JsonObject {
- type?: string
- value?: string
-}
-
-interface VariableCharacteristicsType extends JsonObject {
- dataType: DataEnumType
- supportsMonitoring: boolean
-}
import type { EmptyObject } from '../../EmptyObject.js'
import type { JsonObject } from '../../JsonType.js'
-import type { CustomDataType } from './Common.js'
+import type { CustomDataType, OCPP20UnitEnumType } from './Common.js'
export enum OCPP20LocationEnumType {
Body = 'Body',
export interface OCPP20UnitOfMeasure extends JsonObject {
customData?: CustomDataType
multiplier?: number // Default: 0
- unit?: string // Default: "Wh"
+ unit?: OCPP20UnitEnumType
}
import type {
BootReasonEnumType,
ChargingStationType,
+ CustomDataType,
InstallCertificateUseEnumType,
- OCPP20ConnectorStatusEnumType,
ReportBaseEnumType,
- ReportDataType,
ResetEnumType,
} from './Common.js'
-import type { OCPP20GetVariableDataType, OCPP20SetVariableDataType } from './Variables.js'
+import type {
+ OCPP20ChargingProfileType,
+ OCPP20ConnectorStatusEnumType,
+ OCPP20IdTokenType,
+} from './Transaction.js'
+import type {
+ OCPP20GetVariableDataType,
+ OCPP20SetVariableDataType,
+ ReportDataType,
+} from './Variables.js'
export enum OCPP20IncomingRequestCommand {
CLEAR_CACHE = 'ClearCache',
export interface OCPP20BootNotificationRequest extends JsonObject {
chargingStation: ChargingStationType
+ customData?: CustomDataType
reason: BootReasonEnumType
}
export type OCPP20ClearCacheRequest = EmptyObject
export interface OCPP20GetBaseReportRequest extends JsonObject {
+ customData?: CustomDataType
reportBase: ReportBaseEnumType
requestId: number
}
export interface OCPP20GetVariablesRequest extends JsonObject {
+ customData?: CustomDataType
getVariableData: OCPP20GetVariableDataType[]
}
export interface OCPP20InstallCertificateRequest extends JsonObject {
certificate: string
certificateType: InstallCertificateUseEnumType
+ customData?: CustomDataType
}
export interface OCPP20NotifyReportRequest extends JsonObject {
+ customData?: CustomDataType
generatedAt: Date
reportData?: ReportDataType[]
requestId: number
tbc?: boolean
}
+export interface OCPP20RequestStartTransactionRequest extends JsonObject {
+ chargingProfile?: OCPP20ChargingProfileType
+ customData?: CustomDataType
+ evseId?: number
+ groupIdToken?: OCPP20IdTokenType
+ idToken: OCPP20IdTokenType
+ remoteStartId: number
+}
+
export interface OCPP20ResetRequest extends JsonObject {
+ customData?: CustomDataType
evseId?: number
type: ResetEnumType
}
export interface OCPP20SetVariablesRequest extends JsonObject {
+ customData?: CustomDataType
setVariableData: OCPP20SetVariableDataType[]
}
export interface OCPP20StatusNotificationRequest extends JsonObject {
connectorId: number
connectorStatus: OCPP20ConnectorStatusEnumType
+ customData?: CustomDataType
evseId: number
timestamp: Date
}
import type { JsonObject } from '../../JsonType.js'
import type { RegistrationStatusEnumType } from '../Common.js'
import type {
+ CustomDataType,
GenericDeviceModelStatusEnumType,
GenericStatusEnumType,
InstallCertificateStatusEnumType,
ResetStatusEnumType,
StatusInfoType,
} from './Common.js'
+import type { RequestStartStopStatusEnumType } from './Transaction.js'
import type { OCPP20GetVariableResultType, OCPP20SetVariableResultType } from './Variables.js'
export interface OCPP20BootNotificationResponse extends JsonObject {
currentTime: Date
+ customData?: CustomDataType
interval: number
status: RegistrationStatusEnumType
statusInfo?: StatusInfoType
}
export interface OCPP20ClearCacheResponse extends JsonObject {
+ customData?: CustomDataType
status: GenericStatusEnumType
statusInfo?: StatusInfoType
}
export interface OCPP20GetBaseReportResponse extends JsonObject {
+ customData?: CustomDataType
status: GenericDeviceModelStatusEnumType
statusInfo?: StatusInfoType
}
export interface OCPP20GetVariablesResponse extends JsonObject {
+ customData?: CustomDataType
getVariableResult: OCPP20GetVariableResultType[]
}
export interface OCPP20HeartbeatResponse extends JsonObject {
currentTime: Date
+ customData?: CustomDataType
}
export interface OCPP20InstallCertificateResponse extends JsonObject {
+ customData?: CustomDataType
status: InstallCertificateStatusEnumType
statusInfo?: StatusInfoType
}
export type OCPP20NotifyReportResponse = EmptyObject
+export interface OCPP20RequestStartTransactionResponse extends JsonObject {
+ customData?: CustomDataType
+ status: RequestStartStopStatusEnumType
+ statusInfo?: StatusInfoType
+ transactionId?: string
+}
+
export interface OCPP20ResetResponse extends JsonObject {
+ customData?: CustomDataType
status: ResetStatusEnumType
statusInfo?: StatusInfoType
}
export interface OCPP20SetVariablesResponse extends JsonObject {
+ customData?: CustomDataType
setVariableResult: OCPP20SetVariableResultType[]
}
--- /dev/null
+import type { EmptyObject } from '../../EmptyObject.js'
+import type { JsonObject } from '../../JsonType.js'
+import type { CustomDataType } from './Common.js'
+import type { OCPP20MeterValue } from './MeterValues.js'
+
+export enum CostKindEnumType {
+ CarbonDioxideEmission = 'CarbonDioxideEmission',
+ RelativePricePercentage = 'RelativePricePercentage',
+ RenewableGenerationPercentage = 'RenewableGenerationPercentage',
+}
+
+export enum OCPP20ChargingProfileKindEnumType {
+ Absolute = 'Absolute',
+ Recurring = 'Recurring',
+ Relative = 'Relative',
+}
+
+export enum OCPP20ChargingProfilePurposeEnumType {
+ ChargingStationExternalConstraints = 'ChargingStationExternalConstraints',
+ ChargingStationMaxProfile = 'ChargingStationMaxProfile',
+ TxDefaultProfile = 'TxDefaultProfile',
+ TxProfile = 'TxProfile',
+}
+
+export enum OCPP20ChargingRateUnitEnumType {
+ A = 'A',
+ W = 'W',
+}
+
+export enum OCPP20ChargingStateEnumType {
+ Charging = 'Charging',
+ EVConnected = 'EVConnected',
+ Idle = 'Idle',
+ SuspendedEV = 'SuspendedEV',
+ SuspendedEVSE = 'SuspendedEVSE',
+}
+
+export enum OCPP20ConnectorEnumType {
+ cCCS1 = 'cCCS1',
+ cCCS2 = 'cCCS2',
+ cG105 = 'cG105',
+ cTesla = 'cTesla',
+ cType1 = 'cType1',
+ cType2 = 'cType2',
+ Other1PhMax16A = 'Other1PhMax16A',
+ Other1PhOver16A = 'Other1PhOver16A',
+ Other3Ph = 'Other3Ph',
+ Pan = 'Pan',
+ s309_1P_16A = 's309-1P-16A',
+ s309_1P_32A = 's309-1P-32A',
+ s309_3P_16A = 's309-3P-16A',
+ s309_3P_32A = 's309-3P-32A',
+ sBS1361 = 'sBS1361',
+ sCEE_7_7 = 'sCEE-7-7',
+ sType2 = 'sType2',
+ sType3 = 'sType3',
+ Undetermined = 'Undetermined',
+ Unknown = 'Unknown',
+ wInductive = 'wInductive',
+ wResonant = 'wResonant',
+}
+
+export enum OCPP20ConnectorStatusEnumType {
+ Available = 'Available',
+ Faulted = 'Faulted',
+ Occupied = 'Occupied',
+ Reserved = 'Reserved',
+ Unavailable = 'Unavailable',
+}
+
+export enum OCPP20IdTokenEnumType {
+ Central = 'Central',
+ eMAID = 'eMAID',
+ ISO14443 = 'ISO14443',
+ ISO15693 = 'ISO15693',
+ KeyCode = 'KeyCode',
+ Local = 'Local',
+ MacAddress = 'MacAddress',
+ NoAuthorization = 'NoAuthorization',
+}
+
+export enum OCPP20ReasonEnumType {
+ DeAuthorized = 'DeAuthorized',
+ EmergencyStop = 'EmergencyStop',
+ EnergyLimitReached = 'EnergyLimitReached',
+ EVDisconnected = 'EVDisconnected',
+ GroundFault = 'GroundFault',
+ ImmediateReset = 'ImmediateReset',
+ Local = 'Local',
+ LocalOutOfCredit = 'LocalOutOfCredit',
+ MasterPass = 'MasterPass',
+ Other = 'Other',
+ OvercurrentFault = 'OvercurrentFault',
+ PowerLoss = 'PowerLoss',
+ PowerQuality = 'PowerQuality',
+ Reboot = 'Reboot',
+ Remote = 'Remote',
+ SOCLimitReached = 'SOCLimitReached',
+ StoppedByEV = 'StoppedByEV',
+ TimeLimitReached = 'TimeLimitReached',
+ Timeout = 'Timeout',
+}
+
+export enum OCPP20RecurrencyKindEnumType {
+ Daily = 'Daily',
+ Weekly = 'Weekly',
+}
+
+export enum OCPP20TransactionEventEnumType {
+ Ended = 'Ended',
+ Started = 'Started',
+ Updated = 'Updated',
+}
+
+export enum OCPP20TriggerReasonEnumType {
+ AbnormalCondition = 'AbnormalCondition',
+ Authorized = 'Authorized',
+ CablePluggedIn = 'CablePluggedIn',
+ ChargingRateChanged = 'ChargingRateChanged',
+ ChargingStateChanged = 'ChargingStateChanged',
+ Deauthorized = 'Deauthorized',
+ EnergyLimitReached = 'EnergyLimitReached',
+ EVCommunicationLost = 'EVCommunicationLost',
+ EVConnectTimeout = 'EVConnectTimeout',
+ EVDeparted = 'EVDeparted',
+ EVDetected = 'EVDetected',
+ MeterValueClock = 'MeterValueClock',
+ MeterValuePeriodic = 'MeterValuePeriodic',
+ RemoteStart = 'RemoteStart',
+ RemoteStop = 'RemoteStop',
+ ResetCommand = 'ResetCommand',
+ SignedDataReceived = 'SignedDataReceived',
+ StopAuthorized = 'StopAuthorized',
+ TimeLimitReached = 'TimeLimitReached',
+ Trigger = 'Trigger',
+ UnlockCommand = 'UnlockCommand',
+}
+
+export enum RequestStartStopStatusEnumType {
+ Accepted = 'Accepted',
+ Rejected = 'Rejected',
+}
+
+export enum TransactionComponentNameType {
+ AuthCacheCtrlr = 'AuthCacheCtrlr',
+ AuthCtrlr = 'AuthCtrlr',
+ LocalAuthListCtrlr = 'LocalAuthListCtrlr',
+ ReservationCtrlr = 'ReservationCtrlr',
+ TokenReader = 'TokenReader',
+ TxCtrlr = 'TxCtrlr',
+}
+
+export enum TransactionReasonCodeEnumType {
+ InvalidIdToken = 'InvalidIdToken',
+ TxInProgress = 'TxInProgress',
+ TxNotFound = 'TxNotFound',
+ TxStarted = 'TxStarted',
+ UnknownTxId = 'UnknownTxId',
+}
+
+export interface AdditionalInfoType extends JsonObject {
+ additionalIdToken: string
+ customData?: CustomDataType
+ type: string
+}
+
+export interface ComponentType extends JsonObject {
+ customData?: CustomDataType
+ evse?: OCPP20EVSEType
+ instance?: string
+ name: string
+}
+
+export interface ConsumptionCostType extends JsonObject {
+ cost: CostType[]
+ customData?: CustomDataType
+ startValue: number
+}
+
+export interface CostType extends JsonObject {
+ amount: number
+ amountMultiplier?: number
+ costKind: CostKindEnumType
+ customData?: CustomDataType
+}
+
+export interface OCPP20ChargingProfileType extends JsonObject {
+ chargingProfileKind: OCPP20ChargingProfileKindEnumType
+ chargingProfilePurpose: OCPP20ChargingProfilePurposeEnumType
+ chargingSchedule: OCPP20ChargingScheduleType[]
+ customData?: CustomDataType
+ id: number
+ recurrencyKind?: OCPP20RecurrencyKindEnumType
+ stackLevel: number
+ transactionId?: string
+ validFrom?: Date
+ validTo?: Date
+}
+
+export interface OCPP20ChargingSchedulePeriodType extends JsonObject {
+ customData?: CustomDataType
+ limit: number
+ numberPhases?: number
+ phaseToUse?: number
+ startPeriod: number
+}
+
+export interface OCPP20ChargingScheduleType extends JsonObject {
+ chargingRateUnit: OCPP20ChargingRateUnitEnumType
+ chargingSchedulePeriod: OCPP20ChargingSchedulePeriodType[]
+ customData?: CustomDataType
+ duration?: number
+ id: number
+ minChargingRate?: number
+ startSchedule?: Date
+}
+
+export interface OCPP20EVSEType extends JsonObject {
+ connectorId?: number
+ customData?: CustomDataType
+ id: number
+}
+
+export interface OCPP20IdTokenType extends JsonObject {
+ additionalInfo?: AdditionalInfoType[]
+ customData?: CustomDataType
+ idToken: string
+ type: OCPP20IdTokenEnumType
+}
+
+export interface OCPP20TransactionEventRequest extends JsonObject {
+ cableMaxCurrent?: number
+ customData?: CustomDataType
+ eventType: OCPP20TransactionEventEnumType
+ evse?: OCPP20EVSEType
+ idToken?: OCPP20IdTokenType
+ meterValue?: OCPP20MeterValue[]
+ numberOfPhasesUsed?: number
+ offline?: boolean
+ reservationId?: number
+ seqNo: number
+ timestamp: Date
+ transactionInfo: OCPP20TransactionType
+ triggerReason: OCPP20TriggerReasonEnumType
+}
+
+export type OCPP20TransactionEventResponse = EmptyObject
+
+export interface OCPP20TransactionType extends JsonObject {
+ chargingState?: OCPP20ChargingStateEnumType
+ customData?: CustomDataType
+ remoteStartId?: number
+ stoppedReason?: OCPP20ReasonEnumType
+ timeSpentCharging?: number
+ transactionId: string
+}
+
+export interface RelativeTimeIntervalType extends JsonObject {
+ customData?: CustomDataType
+ duration?: number
+ start: number
+}
+
+export interface SalesTariffEntryType extends JsonObject {
+ consumptionCost?: ConsumptionCostType[]
+ customData?: CustomDataType
+ relativeTimeInterval: RelativeTimeIntervalType
+}
+
+export interface SalesTariffType extends JsonObject {
+ customData?: CustomDataType
+ id: number
+ numEPriceLevels?: number
+ salesTariffDescription?: string
+ salesTariffEntry: SalesTariffEntryType[]
+}
import type { JsonObject } from '../../JsonType.js'
-import type { ComponentType, StatusInfoType } from './Common.js'
+import type { CustomDataType, StatusInfoType } from './Common.js'
+import type { ComponentType } from './Transaction.js'
export enum AttributeEnumType {
Actual = 'Actual',
export interface OCPP20GetVariableDataType extends JsonObject {
attributeType?: AttributeEnumType
component: ComponentType
+ customData?: CustomDataType
variable: VariableType
}
attributeType?: AttributeEnumType
attributeValue?: string
component: ComponentType
+ customData?: CustomDataType
variable: VariableType
}
attributeType?: AttributeEnumType
attributeValue: string
component: ComponentType
+ customData?: CustomDataType
variable: VariableType
}
attributeStatusInfo?: StatusInfoType
attributeType?: AttributeEnumType
component: ComponentType
+ customData?: CustomDataType
variable: VariableType
}
-export interface VariableType extends JsonObject {
- instance?: string
- name: VariableName
+export interface ReportDataType extends JsonObject {
+ component: ComponentType
+ customData?: CustomDataType
+ variable: VariableType
+ variableAttribute?: VariableAttributeType[]
+ variableCharacteristics?: VariableCharacteristicsType
}
-type VariableName =
+export type VariableName =
| OCPP20DeviceInfoVariableName
| OCPP20OptionalVariableName
| OCPP20RequiredVariableName
| OCPP20VendorVariableName
| string
+
+export interface VariableType extends JsonObject {
+ customData?: CustomDataType
+ instance?: string
+ name: VariableName
+}
+
+interface VariableAttributeType extends JsonObject {
+ type?: string
+ value?: string
+}
+
+interface VariableCharacteristicsType extends JsonObject {
+ dataType: string
+ supportsMonitoring: boolean
+}
type OCPP16ChargingSchedulePeriod,
OCPP16RecurrencyKindType,
} from './1.6/ChargingProfile.js'
+import {
+ OCPP20ChargingProfileKindEnumType,
+ OCPP20ChargingProfilePurposeEnumType,
+ type OCPP20ChargingProfileType,
+ OCPP20ChargingRateUnitEnumType,
+ type OCPP20ChargingSchedulePeriodType,
+ OCPP20RecurrencyKindEnumType,
+} from './2.0/Transaction.js'
-export type ChargingProfile = OCPP16ChargingProfile
+export type ChargingProfile = OCPP16ChargingProfile | OCPP20ChargingProfileType
-export type ChargingSchedulePeriod = OCPP16ChargingSchedulePeriod
+export type ChargingSchedulePeriod = OCPP16ChargingSchedulePeriod | OCPP20ChargingSchedulePeriodType
export const ChargingProfilePurposeType = {
...OCPP16ChargingProfilePurposeType,
+ ...OCPP20ChargingProfilePurposeEnumType,
} as const
// eslint-disable-next-line @typescript-eslint/no-redeclare
-export type ChargingProfilePurposeType = OCPP16ChargingProfilePurposeType
+export type ChargingProfilePurposeType =
+ | OCPP16ChargingProfilePurposeType
+ | OCPP20ChargingProfilePurposeEnumType
export const ChargingProfileKindType = {
...OCPP16ChargingProfileKindType,
+ ...OCPP20ChargingProfileKindEnumType,
} as const
// eslint-disable-next-line @typescript-eslint/no-redeclare
-export type ChargingProfileKindType = OCPP16ChargingProfileKindType
+export type ChargingProfileKindType =
+ | OCPP16ChargingProfileKindType
+ | OCPP20ChargingProfileKindEnumType
export const RecurrencyKindType = {
...OCPP16RecurrencyKindType,
+ ...OCPP20RecurrencyKindEnumType,
} as const
// eslint-disable-next-line @typescript-eslint/no-redeclare
-export type RecurrencyKindType = OCPP16RecurrencyKindType
+export type RecurrencyKindType = OCPP16RecurrencyKindType | OCPP20RecurrencyKindEnumType
export const ChargingRateUnitType = {
...OCPP16ChargingRateUnitType,
+ ...OCPP20ChargingRateUnitEnumType,
} as const
// eslint-disable-next-line @typescript-eslint/no-redeclare
-export type ChargingRateUnitType = OCPP16ChargingRateUnitType
+export type ChargingRateUnitType = OCPP16ChargingRateUnitType | OCPP20ChargingRateUnitEnumType
-import { OCPP20ConnectorEnumType } from './2.0/Common.js'
+import { OCPP20ConnectorEnumType } from './2.0/Transaction.js'
export const ConnectorEnumType = {
...OCPP20ConnectorEnumType,
import { OCPP16ChargePointStatus } from './1.6/ChargePointStatus.js'
-import { OCPP20ConnectorStatusEnumType } from './2.0/Common.js'
+import { OCPP20ConnectorStatusEnumType } from './2.0/Transaction.js'
export const ConnectorStatusEnum = {
...OCPP16ChargePointStatus,
OCPP16MeterValueUnit,
type OCPP16SampledValue,
} from './1.6/MeterValues.js'
+import { OCPP20UnitEnumType } from './2.0/Common.js'
import {
OCPP20LocationEnumType,
OCPP20MeasurandEnumType,
+ type OCPP20MeterValue,
OCPP20PhaseEnumType,
OCPP20ReadingContextEnumType,
+ type OCPP20SampledValue,
} from './2.0/MeterValues.js'
-export type MeterValue = OCPP16MeterValue
+export type MeterValue = OCPP16MeterValue | OCPP20MeterValue
export const MeterValueUnit = {
...OCPP16MeterValueUnit,
+ ...OCPP20UnitEnumType,
} as const
// eslint-disable-next-line @typescript-eslint/no-redeclare
-export type MeterValueUnit = OCPP16MeterValueUnit
+export type MeterValueUnit = OCPP16MeterValueUnit | OCPP20UnitEnumType
export const MeterValueContext = {
...OCPP16MeterValueContext,
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type MeterValuePhase = OCPP16MeterValuePhase | OCPP20PhaseEnumType
-export type SampledValue = OCPP16SampledValue
+export type SampledValue = OCPP16SampledValue | OCPP20SampledValue
--- /dev/null
+import { expect } from '@std/expect'
+import { describe, it } from 'node:test'
+
+import { getHashId } from '../src/charging-station/Helpers.js'
+import { AvailabilityType, ConnectorStatusEnum, OCPPVersion } from '../src/types/index.js'
+import { createChargingStation, createChargingStationTemplate } from './ChargingStationFactory.js'
+
+await describe('ChargingStationFactory', async () => {
+ await describe('OCPP Service Mocking', async () => {
+ await it('Should throw explicit error when ocppRequestService is accessed without being mocked', async () => {
+ const station = createChargingStation({ connectorsCount: 1 })
+
+ await expect(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
+ (station as any).ocppRequestService.requestHandler()
+ ).rejects.toThrow(
+ 'ocppRequestService.requestHandler not mocked. Define in createChargingStation options.'
+ )
+ })
+
+ await it('Should throw explicit error when ocppIncomingRequestService is accessed without being mocked', () => {
+ const station = createChargingStation({ connectorsCount: 1 })
+
+ expect(() => {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
+ ;(station as any).ocppIncomingRequestService.stop()
+ }).toThrow(
+ 'ocppIncomingRequestService.stop not mocked. Define in createChargingStation options.'
+ )
+ })
+
+ await it('Should allow custom ocppRequestService mock', async () => {
+ const mockRequestHandler = async () => {
+ return Promise.resolve({ success: true })
+ }
+
+ const station = createChargingStation({
+ connectorsCount: 1,
+ ocppRequestService: {
+ requestHandler: mockRequestHandler,
+ },
+ })
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
+ const result = await (station as any).ocppRequestService.requestHandler()
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ expect(result.success).toBe(true)
+ })
+
+ await it('Should allow custom ocppIncomingRequestService mock', () => {
+ let stopCalled = false
+ const station = createChargingStation({
+ connectorsCount: 1,
+ ocppIncomingRequestService: {
+ stop: () => {
+ stopCalled = true
+ },
+ },
+ })
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
+ ;(station as any).ocppIncomingRequestService.stop()
+ expect(stopCalled).toBe(true)
+ })
+
+ await it('Should throw explicit error when ocppRequestService.sendError is accessed without being mocked', async () => {
+ const station = createChargingStation({ connectorsCount: 1 })
+
+ await expect(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
+ (station as any).ocppRequestService.sendError()
+ ).rejects.toThrow(
+ 'ocppRequestService.sendError not mocked. Define in createChargingStation options.'
+ )
+ })
+
+ await it('Should throw explicit error when ocppRequestService.sendResponse is accessed without being mocked', async () => {
+ const station = createChargingStation({ connectorsCount: 1 })
+
+ await expect(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
+ (station as any).ocppRequestService.sendResponse()
+ ).rejects.toThrow(
+ 'ocppRequestService.sendResponse not mocked. Define in createChargingStation options.'
+ )
+ })
+
+ await it('Should allow custom ocppRequestService.sendError mock', async () => {
+ const mockSendError = async () => {
+ return Promise.resolve({ error: 'test-error' })
+ }
+
+ const station = createChargingStation({
+ connectorsCount: 1,
+ ocppRequestService: {
+ sendError: mockSendError,
+ },
+ })
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
+ const result = await (station as any).ocppRequestService.sendError()
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ expect(result.error).toBe('test-error')
+ })
+
+ await it('Should allow custom ocppRequestService.sendResponse mock', async () => {
+ const mockSendResponse = async () => {
+ return Promise.resolve({ response: 'test-response' })
+ }
+
+ const station = createChargingStation({
+ connectorsCount: 1,
+ ocppRequestService: {
+ sendResponse: mockSendResponse,
+ },
+ })
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
+ const result = await (station as any).ocppRequestService.sendResponse()
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ expect(result.response).toBe('test-response')
+ })
+
+ await it('Should throw explicit error when ocppIncomingRequestService.incomingRequestHandler is accessed without being mocked', async () => {
+ const station = createChargingStation({ connectorsCount: 1 })
+
+ await expect(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
+ (station as any).ocppIncomingRequestService.incomingRequestHandler()
+ ).rejects.toThrow(
+ 'ocppIncomingRequestService.incomingRequestHandler not mocked. Define in createChargingStation options.'
+ )
+ })
+
+ await it('Should allow custom ocppIncomingRequestService.incomingRequestHandler mock', async () => {
+ const mockIncomingRequestHandler = async () => {
+ return Promise.resolve({ handled: true })
+ }
+
+ const station = createChargingStation({
+ connectorsCount: 1,
+ ocppIncomingRequestService: {
+ incomingRequestHandler: mockIncomingRequestHandler,
+ },
+ })
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
+ const result = await (station as any).ocppIncomingRequestService.incomingRequestHandler()
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ expect(result.handled).toBe(true)
+ })
+ })
+
+ await describe('Configuration Validation', async () => {
+ await describe('StationInfo Properties', async () => {
+ await it('Should create station with valid stationInfo', () => {
+ const station = createChargingStation({
+ connectorsCount: 1,
+ stationInfo: {
+ baseName: 'test-base',
+ chargingStationId: 'test-station-001',
+ hashId: 'test-hash',
+ ocppVersion: OCPPVersion.VERSION_16,
+ templateHash: 'template-hash-123',
+ },
+ })
+
+ expect(station.stationInfo?.chargingStationId).toBe('test-station-001')
+ expect(station.stationInfo?.hashId).toBe('test-hash')
+ expect(station.stationInfo?.baseName).toBe('test-base')
+ expect(station.stationInfo?.ocppVersion).toBe(OCPPVersion.VERSION_16)
+ expect(station.stationInfo?.templateHash).toBe('template-hash-123')
+ })
+
+ await it('Should validate stationInfo properties via Helpers', () => {
+ // These tests are covered by the comprehensive validation tests
+ // in Helpers.test.ts where properties are tested with undefined values
+ const station = createChargingStation({
+ connectorsCount: 1,
+ stationInfo: {
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ })
+
+ expect(station.stationInfo?.ocppVersion).toBe(OCPPVersion.VERSION_201)
+ })
+ })
+
+ await describe('Connector Configuration', async () => {
+ await it('Should create station with no connectors when connectorsCount is 0', () => {
+ const station = createChargingStation({
+ connectorsCount: 0,
+ })
+
+ // Verify no connectors exist (connector map should be empty except for connector 0 if EVSEs are used)
+ expect(station.connectors.size).toBe(0)
+ })
+
+ await it('Should create station with specified number of connectors', () => {
+ const station = createChargingStation({
+ connectorsCount: 3,
+ })
+
+ // Should have 4 connectors (0, 1, 2, 3) when not using EVSEs
+ expect(station.connectors.size).toBe(4)
+ })
+
+ await it('Should handle connector status properly', () => {
+ const station = createChargingStation({
+ connectorsCount: 2,
+ })
+
+ // Verify connectors are properly initialized
+ expect(station.getConnectorStatus(1)).toBeDefined()
+ expect(station.getConnectorStatus(2)).toBeDefined()
+ })
+
+ await it('Should create station with custom connector defaults', () => {
+ const station = createChargingStation({
+ connectorDefaults: {
+ availability: AvailabilityType.Inoperative,
+ status: ConnectorStatusEnum.Unavailable,
+ },
+ connectorsCount: 1,
+ })
+
+ const connectorStatus = station.getConnectorStatus(1)
+ expect(connectorStatus?.availability).toBe(AvailabilityType.Inoperative)
+ expect(connectorStatus?.status).toBe(ConnectorStatusEnum.Unavailable)
+ })
+ })
+
+ await describe('OCPP Version-Specific Configuration', async () => {
+ await it('Should configure OCPP 1.6 station correctly', () => {
+ const station = createChargingStation({
+ connectorsCount: 2,
+ stationInfo: {
+ ocppVersion: OCPPVersion.VERSION_16,
+ },
+ })
+
+ expect(station.stationInfo?.ocppVersion).toBe(OCPPVersion.VERSION_16)
+ expect(station.connectors.size).toBe(3) // 0 + 2 connectors
+ expect(station.hasEvses).toBe(false)
+ })
+
+ await it('Should configure OCPP 2.0 station with EVSEs', () => {
+ const station = createChargingStation({
+ connectorsCount: 0, // OCPP 2.0 uses EVSEs instead of connectors
+ stationInfo: {
+ ocppVersion: OCPPVersion.VERSION_20,
+ },
+ })
+
+ expect(station.stationInfo?.ocppVersion).toBe(OCPPVersion.VERSION_20)
+ expect(station.connectors.size).toBe(0)
+ expect(station.hasEvses).toBe(true)
+ })
+
+ await it('Should configure OCPP 2.0.1 station with EVSEs', () => {
+ const station = createChargingStation({
+ connectorsCount: 0, // OCPP 2.0.1 uses EVSEs instead of connectors
+ stationInfo: {
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ })
+
+ expect(station.stationInfo?.ocppVersion).toBe(OCPPVersion.VERSION_201)
+ expect(station.connectors.size).toBe(0)
+ expect(station.hasEvses).toBe(true)
+ })
+ })
+
+ await describe('EVSE Configuration', async () => {
+ await it('Should create station with EVSEs when configuration is provided', () => {
+ const station = createChargingStation({
+ connectorsCount: 6,
+ evseConfiguration: {
+ evsesCount: 2,
+ },
+ })
+
+ expect(station.hasEvses).toBe(true)
+ expect(station.evses.size).toBe(2)
+ expect(station.connectors.size).toBe(7) // 0 + 6 connectors
+ })
+
+ await it('Should automatically enable EVSEs for OCPP 2.0+ versions', () => {
+ const station = createChargingStation({
+ connectorsCount: 3,
+ stationInfo: {
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ })
+
+ expect(station.hasEvses).toBe(true)
+ expect(station.connectors.size).toBe(4) // 0 + 3 connectors
+ })
+ })
+
+ await describe('Factory Default Values', async () => {
+ await it('Should provide sensible defaults for all required properties', () => {
+ const station = createChargingStation({
+ connectorsCount: 1,
+ })
+
+ // Verify factory provides all required defaults
+ expect(station.stationInfo?.chargingStationId).toBeDefined()
+ expect(station.stationInfo?.hashId).toBeDefined()
+ expect(station.stationInfo?.baseName).toBeDefined()
+ expect(station.stationInfo?.ocppVersion).toBeDefined()
+ expect(station.stationInfo?.templateHash).toBeUndefined() // Factory doesn't set templateHash by default
+ })
+
+ await it('Should allow overriding factory defaults', () => {
+ const customStationId = 'custom-station-123'
+ const customHashId = 'custom-hash-456'
+
+ const station = createChargingStation({
+ connectorsCount: 1,
+ stationInfo: {
+ chargingStationId: customStationId,
+ hashId: customHashId,
+ },
+ })
+
+ expect(station.stationInfo?.chargingStationId).toBe(customStationId)
+ expect(station.stationInfo?.hashId).toBe(customHashId)
+ // Other defaults should still be provided
+ expect(station.stationInfo?.baseName).toBeDefined()
+ expect(station.stationInfo?.ocppVersion).toBeDefined()
+ })
+
+ await it('Should use default base name when not provided', () => {
+ const station = createChargingStation({
+ connectorsCount: 1,
+ })
+
+ expect(station.stationInfo?.baseName).toBe('CS-TEST')
+ expect(station.stationInfo?.chargingStationId).toBe('CS-TEST-00001')
+ })
+
+ await it('Should use custom base name when provided', () => {
+ const customBaseName = 'CUSTOM-STATION'
+ const station = createChargingStation({
+ baseName: customBaseName,
+ connectorsCount: 1,
+ })
+
+ expect(station.stationInfo?.baseName).toBe(customBaseName)
+ expect(station.stationInfo?.chargingStationId).toBe('CUSTOM-STATION-00001')
+ })
+ })
+
+ await describe('Configuration Options', async () => {
+ await it('Should respect connection timeout setting', () => {
+ const customTimeout = 45000
+ const station = createChargingStation({
+ connectionTimeout: customTimeout,
+ connectorsCount: 1,
+ })
+
+ expect(station.getConnectionTimeout()).toBe(customTimeout)
+ })
+
+ await it('Should respect heartbeat interval setting', () => {
+ const customInterval = 120000
+ const station = createChargingStation({
+ connectorsCount: 1,
+ heartbeatInterval: customInterval,
+ })
+
+ expect(station.getHeartbeatInterval()).toBe(customInterval)
+ })
+
+ await it('Should respect websocket ping interval setting', () => {
+ const customPingInterval = 90000
+ const station = createChargingStation({
+ connectorsCount: 1,
+ websocketPingInterval: customPingInterval,
+ })
+
+ expect(station.getWebSocketPingInterval()).toBe(customPingInterval)
+ })
+
+ await it('Should respect started and starting flags', () => {
+ const station = createChargingStation({
+ connectorsCount: 1,
+ started: true,
+ starting: false,
+ })
+
+ expect(station.started).toBe(true)
+ expect(station.starting).toBe(false)
+ })
+ })
+
+ await describe('Integration with Helpers', async () => {
+ await it('Should properly integrate with helper functions', () => {
+ const station = createChargingStation({
+ connectorsCount: 1,
+ stationInfo: {
+ baseName: 'HELPER-TEST',
+ chargingStationId: 'HELPER-TEST-001',
+ },
+ })
+
+ // Verify the station info is properly set
+ expect(station.stationInfo?.chargingStationId).toBe('HELPER-TEST-001')
+
+ // Verify hash ID generation works with the helpers
+ const template = createChargingStationTemplate('HELPER-TEST')
+ const hashId = getHashId(1, template)
+ expect(hashId).toBeDefined()
+ expect(typeof hashId).toBe('string')
+ })
+ })
+ })
+})
import { millisecondsToSeconds } from 'date-fns'
import type { ChargingStation } from '../src/charging-station/index.js'
-import type {
- ChargingStationConfiguration,
- ChargingStationInfo,
- ChargingStationTemplate,
-} from '../src/types/index.js'
+import { IdTagsCache } from '../src/charging-station/IdTagsCache.js'
import {
- OCPP20ConnectorStatusEnumType,
+ AvailabilityType,
+ type BootNotificationResponse,
+ type ChargingProfile,
+ type ChargingStationConfiguration,
+ type ChargingStationInfo,
+ type ChargingStationTemplate,
+ ConnectorStatusEnum,
OCPP20OptionalVariableName,
OCPPVersion,
+ RegistrationStatusEnumType,
+ type SampledValueTemplate,
} from '../src/types/index.js'
-import { Constants } from '../src/utils/index.js'
+import { clone, Constants } from '../src/utils/index.js'
/**
* Options to customize the construction of a ChargingStation test instance
+ * @example createChargingStation({ connectorsCount: 2, ocppRequestService: mockService })
*/
export interface ChargingStationOptions {
baseName?: string
connectionTimeout?: number
- hasEvses?: boolean
+ connectorDefaults?: {
+ availability?: AvailabilityType
+ status?: ConnectorStatusEnum
+ }
+ /** Number of connectors to create (default: 3 if EVSEs enabled, 0 otherwise) */
+ connectorsCount?: number
+ /** EVSE configuration for OCPP 2.0 - enables EVSE mode when present */
+ evseConfiguration?: {
+ evsesCount?: number
+ }
+
heartbeatInterval?: number
ocppConfiguration?: ChargingStationConfiguration
+ /** Custom OCPP incoming request service for test mocking */
+ ocppIncomingRequestService?: unknown
+ /** Custom OCPP request service for test mocking */
+ ocppRequestService?: unknown
started?: boolean
starting?: boolean
stationInfo?: Partial<ChargingStationInfo>
/**
* Creates a ChargingStation instance for tests
- * @param options - Options to customize the ChargingStation instance
- * @returns A mock ChargingStation instance
+ * @param options - Configuration options for the charging station
+ * @returns ChargingStation instance configured for testing
*/
export function createChargingStation (options: ChargingStationOptions = {}): ChargingStation {
const baseName = options.baseName ?? CHARGING_STATION_BASE_NAME
const heartbeatInterval = options.heartbeatInterval ?? Constants.DEFAULT_HEARTBEAT_INTERVAL
const websocketPingInterval =
options.websocketPingInterval ?? Constants.DEFAULT_WEBSOCKET_PING_INTERVAL
+ const useEvses = determineEvseUsage(options)
+ const connectorsCount = options.connectorsCount ?? (useEvses ? 3 : 0)
+ const { connectors, evses } = createConnectorsConfiguration(options, connectorsCount, useEvses)
- return {
- connectors: new Map(),
- evses: new Map(),
+ const chargingStation = {
+ bootNotificationResponse: {
+ currentTime: new Date(),
+ interval: heartbeatInterval,
+ status: RegistrationStatusEnumType.ACCEPTED,
+ } as BootNotificationResponse,
+ connectors,
+ emitChargingStationEvent: () => {
+ /* no-op for tests */
+ },
+ evses,
getConnectionTimeout: () => connectionTimeout,
+ getConnectorStatus: (connectorId: number) => {
+ if (chargingStation.hasEvses) {
+ for (const evseStatus of chargingStation.evses.values()) {
+ if (evseStatus.connectors.has(connectorId)) {
+ return evseStatus.connectors.get(connectorId)
+ }
+ }
+ return undefined
+ }
+ return chargingStation.connectors.get(connectorId)
+ },
getHeartbeatInterval: () => heartbeatInterval,
getWebSocketPingInterval: () => websocketPingInterval,
- hasEvses: options.hasEvses ?? false,
- inAcceptedState: () => true,
- logPrefix: () => `${baseName} |`,
- ocppConfiguration: options.ocppConfiguration ?? {
+ hasEvses: useEvses,
+ idTagsCache: IdTagsCache.getInstance(),
+ inAcceptedState: (): boolean => {
+ return (
+ chargingStation.bootNotificationResponse?.status === RegistrationStatusEnumType.ACCEPTED
+ )
+ },
+ logPrefix: (): string => {
+ const stationId =
+ chargingStation.stationInfo?.chargingStationId ??
+ `${baseName}-0000${templateIndex.toString()}`
+ return `${stationId} |`
+ },
+ ocppConfiguration: {
configurationKey: [
{
key: OCPP20OptionalVariableName.WebSocketPingInterval,
value: millisecondsToSeconds(heartbeatInterval).toString(),
},
],
+ ...options.ocppConfiguration,
+ },
+ ocppIncomingRequestService: options.ocppIncomingRequestService ?? {
+ incomingRequestHandler: async () => {
+ return await Promise.reject(
+ new Error(
+ 'ocppIncomingRequestService.incomingRequestHandler not mocked. Define in createChargingStation options.'
+ )
+ )
+ },
+ stop: () => {
+ throw new Error(
+ 'ocppIncomingRequestService.stop not mocked. Define in createChargingStation options.'
+ )
+ },
+ },
+ ocppRequestService: options.ocppRequestService ?? {
+ requestHandler: async () => {
+ return await Promise.reject(
+ new Error(
+ 'ocppRequestService.requestHandler not mocked. Define in createChargingStation options.'
+ )
+ )
+ },
+ sendError: async () => {
+ return await Promise.reject(
+ new Error(
+ 'ocppRequestService.sendError not mocked. Define in createChargingStation options.'
+ )
+ )
+ },
+ sendResponse: async () => {
+ return await Promise.reject(
+ new Error(
+ 'ocppRequestService.sendResponse not mocked. Define in createChargingStation options.'
+ )
+ )
+ },
},
restartHeartbeat: () => {
/* no-op for tests */
/* no-op for tests */
},
started: options.started ?? false,
- starting: options.starting,
+ starting: options.starting ?? false,
stationInfo: {
baseName,
chargingStationId: `${baseName}-00001`,
hashId: 'test-hash-id',
maximumAmperage: 16,
maximumPower: 12000,
+ ocppVersion: OCPPVersion.VERSION_16,
templateIndex,
templateName: 'test-template.json',
...options.stationInfo,
- },
- wsConnection: {
- pingInterval: websocketPingInterval,
- },
+ } as ChargingStationInfo,
} as unknown as ChargingStation
+
+ return chargingStation
}
/**
* Creates a ChargingStation template for tests
- * @param baseName - Base name for the template
- * @returns A ChargingStationTemplate instance
+ * @param baseName - Base name for the charging station
+ * @returns ChargingStation template for testing
*/
export function createChargingStationTemplate (
baseName = CHARGING_STATION_BASE_NAME
}
/**
- * Creates a ChargingStation instance with connectors and EVSEs configured for OCPP 2.0
- * @param options - Options to customize the ChargingStation instance
- * @returns A mock ChargingStation instance with EVSEs
+ * Creates connector and EVSE configuration
+ * @param options - Configuration options
+ * @param connectorsCount - Number of connectors to create
+ * @param useEvses - Whether to use EVSE mode
+ * @returns Object containing connectors and evses maps
*/
-export function createChargingStationWithEvses (
- options: ChargingStationOptions = {}
-): ChargingStation {
- const chargingStation = createChargingStation({
- hasEvses: true,
- stationInfo: {
- ocppVersion: OCPPVersion.VERSION_201,
- ...options.stationInfo,
- },
- ...options,
- })
-
- // Add default connectors and EVSEs
- Object.assign(chargingStation, {
- connectors: new Map([
- [1, { status: OCPP20ConnectorStatusEnumType.Available }],
- [2, { status: OCPP20ConnectorStatusEnumType.Available }],
- ]),
- evses: new Map([
- [1, { connectors: new Map([[1, {}]]) }],
- [2, { connectors: new Map([[1, {}]]) }],
- ]),
- })
+function createConnectorsConfiguration (
+ options: ChargingStationOptions,
+ connectorsCount: number,
+ useEvses: boolean
+) {
+ const connectors = new Map()
+ const evses = new Map()
- return chargingStation
+ if (connectorsCount === 0) {
+ return { connectors, evses }
+ }
+
+ const createConnectorStatus = (connectorId: number) => {
+ const baseStatus = {
+ availability: options.connectorDefaults?.availability ?? AvailabilityType.Operative,
+ chargingProfiles: [] as ChargingProfile[],
+ energyActiveImportRegisterValue: 0,
+ idTagAuthorized: false,
+ idTagLocalAuthorized: false,
+ MeterValues: [] as SampledValueTemplate[],
+ status: options.connectorDefaults?.status ?? ConnectorStatusEnum.Available,
+ transactionEnergyActiveImportRegisterValue: 0,
+ transactionId: undefined,
+ transactionIdTag: undefined,
+ transactionRemoteStarted: false,
+ transactionStart: undefined,
+ transactionStarted: false,
+ }
+
+ return clone(baseStatus)
+ }
+
+ if (useEvses) {
+ const evsesCount = options.evseConfiguration?.evsesCount ?? connectorsCount
+ const connectorsCountPerEvse = Math.ceil(connectorsCount / evsesCount)
+
+ const connector0 = createConnectorStatus(0)
+ connectors.set(0, connector0)
+
+ for (let evseId = 1; evseId <= evsesCount; evseId++) {
+ const evseConnectors = new Map()
+ const startConnectorId = (evseId - 1) * connectorsCountPerEvse + 1
+ const endConnectorId = Math.min(
+ startConnectorId + connectorsCountPerEvse - 1,
+ connectorsCount
+ )
+
+ for (let connectorId = startConnectorId; connectorId <= endConnectorId; connectorId++) {
+ const connectorStatus = createConnectorStatus(connectorId)
+ connectors.set(connectorId, connectorStatus)
+ evseConnectors.set(connectorId, clone(connectorStatus))
+ }
+
+ evses.set(evseId, {
+ availability: AvailabilityType.Operative,
+ connectors: evseConnectors,
+ })
+ }
+ } else {
+ for (let connectorId = 0; connectorId <= connectorsCount; connectorId++) {
+ connectors.set(connectorId, createConnectorStatus(connectorId))
+ }
+ }
+
+ return { connectors, evses }
+}
+
+/**
+ * Determines whether EVSEs should be used based on configuration
+ * @param options - Configuration options to check
+ * @returns True if EVSEs should be used, false otherwise
+ */
+function determineEvseUsage (options: ChargingStationOptions): boolean {
+ return (
+ options.evseConfiguration?.evsesCount != null ||
+ options.stationInfo?.ocppVersion === OCPPVersion.VERSION_20 ||
+ options.stationInfo?.ocppVersion === OCPPVersion.VERSION_201
+ )
}
await describe('Helpers test suite', async () => {
const baseName = 'CS-TEST'
const chargingStationTemplate = createChargingStationTemplate(baseName)
- const chargingStation = createChargingStation({ baseName })
await it('Verify getChargingStationId()', () => {
expect(getChargingStationId(1, chargingStationTemplate)).toBe(`${baseName}-00001`)
)
})
- await it('Verify validateStationInfo()', () => {
+ await it('Verify validateStationInfo() - Missing stationInfo', () => {
+ // For validation edge cases, we need to manually create invalid states
+ // since the factory is designed to create valid configurations
+ const stationNoInfo = createChargingStation({ baseName })
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- delete (chargingStation as any).stationInfo
+ delete (stationNoInfo as any).stationInfo
expect(() => {
- validateStationInfo(chargingStation)
+ validateStationInfo(stationNoInfo)
}).toThrow(new BaseError('Missing charging station information'))
- chargingStation.stationInfo = {} as ChargingStationInfo
+ })
+
+ await it('Verify validateStationInfo() - Empty stationInfo', () => {
+ // For validation edge cases, manually create empty stationInfo
+ const stationEmptyInfo = createChargingStation({ baseName })
+ stationEmptyInfo.stationInfo = {} as ChargingStationInfo
expect(() => {
- validateStationInfo(chargingStation)
+ validateStationInfo(stationEmptyInfo)
}).toThrow(new BaseError('Missing charging station information'))
- chargingStation.stationInfo.baseName = baseName
+ })
+
+ await it('Verify validateStationInfo() - Missing chargingStationId', () => {
+ const stationMissingId = createChargingStation({
+ baseName,
+ stationInfo: { baseName, chargingStationId: undefined },
+ })
expect(() => {
- validateStationInfo(chargingStation)
+ validateStationInfo(stationMissingId)
}).toThrow(new BaseError('Missing chargingStationId in stationInfo properties'))
- chargingStation.stationInfo.chargingStationId = ''
+ })
+
+ await it('Verify validateStationInfo() - Empty chargingStationId', () => {
+ const stationEmptyId = createChargingStation({
+ baseName,
+ stationInfo: { baseName, chargingStationId: '' },
+ })
expect(() => {
- validateStationInfo(chargingStation)
+ validateStationInfo(stationEmptyId)
}).toThrow(new BaseError('Missing chargingStationId in stationInfo properties'))
- chargingStation.stationInfo.chargingStationId = getChargingStationId(1, chargingStationTemplate)
+ })
+
+ await it('Verify validateStationInfo() - Missing hashId', () => {
+ const stationMissingHash = createChargingStation({
+ baseName,
+ stationInfo: {
+ baseName,
+ chargingStationId: getChargingStationId(1, chargingStationTemplate),
+ hashId: undefined,
+ },
+ })
expect(() => {
- validateStationInfo(chargingStation)
+ validateStationInfo(stationMissingHash)
}).toThrow(new BaseError(`${baseName}-00001: Missing hashId in stationInfo properties`))
- chargingStation.stationInfo.hashId = ''
+ })
+
+ await it('Verify validateStationInfo() - Empty hashId', () => {
+ const stationEmptyHash = createChargingStation({
+ baseName,
+ stationInfo: {
+ baseName,
+ chargingStationId: getChargingStationId(1, chargingStationTemplate),
+ hashId: '',
+ },
+ })
expect(() => {
- validateStationInfo(chargingStation)
+ validateStationInfo(stationEmptyHash)
}).toThrow(new BaseError(`${baseName}-00001: Missing hashId in stationInfo properties`))
- chargingStation.stationInfo.hashId = getHashId(1, chargingStationTemplate)
+ })
+
+ await it('Verify validateStationInfo() - Missing templateIndex', () => {
+ const stationMissingTemplate = createChargingStation({
+ baseName,
+ stationInfo: {
+ baseName,
+ chargingStationId: getChargingStationId(1, chargingStationTemplate),
+ hashId: getHashId(1, chargingStationTemplate),
+ templateIndex: undefined,
+ },
+ })
expect(() => {
- validateStationInfo(chargingStation)
+ validateStationInfo(stationMissingTemplate)
}).toThrow(new BaseError(`${baseName}-00001: Missing templateIndex in stationInfo properties`))
- chargingStation.stationInfo.templateIndex = 0
+ })
+
+ await it('Verify validateStationInfo() - Invalid templateIndex (zero)', () => {
+ const stationInvalidTemplate = createChargingStation({
+ baseName,
+ stationInfo: {
+ baseName,
+ chargingStationId: getChargingStationId(1, chargingStationTemplate),
+ hashId: getHashId(1, chargingStationTemplate),
+ templateIndex: 0,
+ },
+ })
expect(() => {
- validateStationInfo(chargingStation)
+ validateStationInfo(stationInvalidTemplate)
}).toThrow(
new BaseError(`${baseName}-00001: Invalid templateIndex value in stationInfo properties`)
)
- chargingStation.stationInfo.templateIndex = 1
+ })
+
+ await it('Verify validateStationInfo() - Missing templateName', () => {
+ const stationMissingName = createChargingStation({
+ baseName,
+ stationInfo: {
+ baseName,
+ chargingStationId: getChargingStationId(1, chargingStationTemplate),
+ hashId: getHashId(1, chargingStationTemplate),
+ templateIndex: 1,
+ templateName: undefined,
+ },
+ })
expect(() => {
- validateStationInfo(chargingStation)
+ validateStationInfo(stationMissingName)
}).toThrow(new BaseError(`${baseName}-00001: Missing templateName in stationInfo properties`))
- chargingStation.stationInfo.templateName = ''
+ })
+
+ await it('Verify validateStationInfo() - Empty templateName', () => {
+ const stationEmptyName = createChargingStation({
+ baseName,
+ stationInfo: {
+ baseName,
+ chargingStationId: getChargingStationId(1, chargingStationTemplate),
+ hashId: getHashId(1, chargingStationTemplate),
+ templateIndex: 1,
+ templateName: '',
+ },
+ })
expect(() => {
- validateStationInfo(chargingStation)
+ validateStationInfo(stationEmptyName)
}).toThrow(new BaseError(`${baseName}-00001: Missing templateName in stationInfo properties`))
- chargingStation.stationInfo.templateName = 'test-template.json'
+ })
+
+ await it('Verify validateStationInfo() - Missing maximumPower', () => {
+ const stationMissingPower = createChargingStation({
+ baseName,
+ stationInfo: {
+ baseName,
+ chargingStationId: getChargingStationId(1, chargingStationTemplate),
+ hashId: getHashId(1, chargingStationTemplate),
+ maximumPower: undefined,
+ templateIndex: 1,
+ templateName: 'test-template.json',
+ },
+ })
expect(() => {
- validateStationInfo(chargingStation)
+ validateStationInfo(stationMissingPower)
}).toThrow(new BaseError(`${baseName}-00001: Missing maximumPower in stationInfo properties`))
- chargingStation.stationInfo.maximumPower = 0
+ })
+
+ await it('Verify validateStationInfo() - Invalid maximumPower (zero)', () => {
+ const stationInvalidPower = createChargingStation({
+ baseName,
+ stationInfo: {
+ baseName,
+ chargingStationId: getChargingStationId(1, chargingStationTemplate),
+ hashId: getHashId(1, chargingStationTemplate),
+ maximumPower: 0,
+ templateIndex: 1,
+ templateName: 'test-template.json',
+ },
+ })
expect(() => {
- validateStationInfo(chargingStation)
+ validateStationInfo(stationInvalidPower)
}).toThrow(
new RangeError(`${baseName}-00001: Invalid maximumPower value in stationInfo properties`)
)
- chargingStation.stationInfo.maximumPower = 12000
+ })
+
+ await it('Verify validateStationInfo() - Missing maximumAmperage', () => {
+ const stationMissingAmperage = createChargingStation({
+ baseName,
+ stationInfo: {
+ baseName,
+ chargingStationId: getChargingStationId(1, chargingStationTemplate),
+ hashId: getHashId(1, chargingStationTemplate),
+ maximumAmperage: undefined,
+ maximumPower: 12000,
+ templateIndex: 1,
+ templateName: 'test-template.json',
+ },
+ })
expect(() => {
- validateStationInfo(chargingStation)
+ validateStationInfo(stationMissingAmperage)
}).toThrow(
new BaseError(`${baseName}-00001: Missing maximumAmperage in stationInfo properties`)
)
- chargingStation.stationInfo.maximumAmperage = 0
+ })
+
+ await it('Verify validateStationInfo() - Invalid maximumAmperage (zero)', () => {
+ const stationInvalidAmperage = createChargingStation({
+ baseName,
+ stationInfo: {
+ baseName,
+ chargingStationId: getChargingStationId(1, chargingStationTemplate),
+ hashId: getHashId(1, chargingStationTemplate),
+ maximumAmperage: 0,
+ maximumPower: 12000,
+ templateIndex: 1,
+ templateName: 'test-template.json',
+ },
+ })
expect(() => {
- validateStationInfo(chargingStation)
+ validateStationInfo(stationInvalidAmperage)
}).toThrow(
new RangeError(`${baseName}-00001: Invalid maximumAmperage value in stationInfo properties`)
)
- chargingStation.stationInfo.maximumAmperage = 16
+ })
+
+ await it('Verify validateStationInfo() - Valid configuration passes', () => {
+ const validStation = createChargingStation({
+ baseName,
+ stationInfo: {
+ baseName,
+ chargingStationId: getChargingStationId(1, chargingStationTemplate),
+ hashId: getHashId(1, chargingStationTemplate),
+ maximumAmperage: 16,
+ maximumPower: 12000,
+ templateIndex: 1,
+ templateName: 'test-template.json',
+ },
+ })
expect(() => {
- validateStationInfo(chargingStation)
+ validateStationInfo(validStation)
}).not.toThrow()
- chargingStation.stationInfo.ocppVersion = OCPPVersion.VERSION_20
+ })
+
+ await it('Verify validateStationInfo() - OCPP 2.0 requires EVSE', () => {
+ const stationOcpp20 = createChargingStation({
+ baseName,
+ connectorsCount: 0, // Ensure no EVSEs are created
+ stationInfo: {
+ baseName,
+ chargingStationId: getChargingStationId(1, chargingStationTemplate),
+ hashId: getHashId(1, chargingStationTemplate),
+ maximumAmperage: 16,
+ maximumPower: 12000,
+ ocppVersion: OCPPVersion.VERSION_20,
+ templateIndex: 1,
+ templateName: 'test-template.json',
+ },
+ })
expect(() => {
- validateStationInfo(chargingStation)
+ validateStationInfo(stationOcpp20)
}).toThrow(
new BaseError(
`${baseName}-00001: OCPP 2.0.x requires at least one EVSE defined in the charging station template/configuration`
)
)
- chargingStation.stationInfo.ocppVersion = OCPPVersion.VERSION_201
+ })
+
+ await it('Verify validateStationInfo() - OCPP 2.0.1 requires EVSE', () => {
+ const stationOcpp201 = createChargingStation({
+ baseName,
+ connectorsCount: 0, // Ensure no EVSEs are created
+ stationInfo: {
+ baseName,
+ chargingStationId: getChargingStationId(1, chargingStationTemplate),
+ hashId: getHashId(1, chargingStationTemplate),
+ maximumAmperage: 16,
+ maximumPower: 12000,
+ ocppVersion: OCPPVersion.VERSION_201,
+ templateIndex: 1,
+ templateName: 'test-template.json',
+ },
+ })
expect(() => {
- validateStationInfo(chargingStation)
+ validateStationInfo(stationOcpp201)
}).toThrow(
new BaseError(
`${baseName}-00001: OCPP 2.0.x requires at least one EVSE defined in the charging station template/configuration`
)
})
- await it('Verify checkChargingStationState()', t => {
+ await it('Verify checkChargingStationState() - Not started or starting', t => {
const warnMock = t.mock.method(logger, 'warn')
- expect(checkChargingStationState(chargingStation, 'log prefix |')).toBe(false)
- expect(warnMock.mock.calls.length).toBe(1)
- chargingStation.starting = true
- expect(checkChargingStationState(chargingStation, 'log prefix |')).toBe(true)
- expect(warnMock.mock.calls.length).toBe(1)
- chargingStation.started = true
- expect(checkChargingStationState(chargingStation, 'log prefix |')).toBe(true)
+ const stationNotStarted = createChargingStation({ baseName, started: false, starting: false })
+ expect(checkChargingStationState(stationNotStarted, 'log prefix |')).toBe(false)
expect(warnMock.mock.calls.length).toBe(1)
})
+ await it('Verify checkChargingStationState() - Starting returns true', t => {
+ const warnMock = t.mock.method(logger, 'warn')
+ const stationStarting = createChargingStation({ baseName, started: false, starting: true })
+ expect(checkChargingStationState(stationStarting, 'log prefix |')).toBe(true)
+ expect(warnMock.mock.calls.length).toBe(0)
+ })
+
+ await it('Verify checkChargingStationState() - Started returns true', t => {
+ const warnMock = t.mock.method(logger, 'warn')
+ const stationStarted = createChargingStation({ baseName, started: true, starting: false })
+ expect(checkChargingStationState(stationStarted, 'log prefix |')).toBe(true)
+ expect(warnMock.mock.calls.length).toBe(0)
+ })
+
await it('Verify getPhaseRotationValue()', () => {
expect(getPhaseRotationValue(0, 0)).toBe('0.RST')
expect(getPhaseRotationValue(1, 0)).toBe('1.NotApplicable')
import { expect } from '@std/expect'
import { describe, it } from 'node:test'
-import { IdTagsCache } from '../../../../src/charging-station/IdTagsCache.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 { createChargingStationWithEvses } from '../../../ChargingStationFactory.js'
-import { TEST_CHARGING_STATION_NAME } from './OCPP20TestConstants.js'
+import { createChargingStation } from '../../../ChargingStationFactory.js'
+import { TEST_CHARGING_STATION_BASE_NAME } from './OCPP20TestConstants.js'
await describe('C11 - Clear Authorization Data in Authorization Cache', async () => {
- const mockChargingStation = createChargingStationWithEvses({
- baseName: TEST_CHARGING_STATION_NAME,
+ const mockChargingStation = createChargingStation({
+ 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,
})
- // Initialize idTagsCache to avoid undefined errors
- mockChargingStation.idTagsCache = IdTagsCache.getInstance()
-
const incomingRequestService = new OCPP20IncomingRequestService()
// FR: C11.FR.01
OCPP20DeviceInfoVariableName,
type OCPP20GetBaseReportRequest,
type OCPP20SetVariableResultType,
+ OCPPVersion,
ReportBaseEnumType,
type ReportDataType,
} from '../../../../src/types/index.js'
} from '../../../../src/types/index.js'
import { StandardParametersKey } from '../../../../src/types/ocpp/Configuration.js'
import { Constants } from '../../../../src/utils/index.js'
-import { createChargingStationWithEvses } from '../../../ChargingStationFactory.js'
+import { createChargingStation } from '../../../ChargingStationFactory.js'
import {
TEST_CHARGE_POINT_MODEL,
TEST_CHARGE_POINT_SERIAL_NUMBER,
TEST_CHARGE_POINT_VENDOR,
- TEST_CHARGING_STATION_NAME,
+ TEST_CHARGING_STATION_BASE_NAME,
TEST_FIRMWARE_VERSION,
} from './OCPP20TestConstants.js'
await describe('B08 - Get Base Report', async () => {
- const mockChargingStation = createChargingStationWithEvses({
- baseName: TEST_CHARGING_STATION_NAME,
+ const mockChargingStation = createChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
stationInfo: {
chargePointModel: TEST_CHARGE_POINT_MODEL,
chargePointVendor: TEST_CHARGE_POINT_VENDOR,
firmwareVersion: TEST_FIRMWARE_VERSION,
ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
},
websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
})
// FR: B08.FR.05
await it('Should return EmptyResultSet when no data is available', () => {
// Create a charging station with minimal configuration
- const minimalChargingStation = createChargingStationWithEvses({
+ const minimalChargingStation = createChargingStation({
baseName: 'CS-MINIMAL',
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
ocppConfiguration: {
configurationKey: [],
},
stationInfo: {
ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
},
})
// FR: B08.FR.09
await it('Should handle GetBaseReport with EVSE structure', () => {
- // The createChargingStationWithEvses should create a station with EVSEs
- const stationWithEvses = createChargingStationWithEvses({
+ // The createChargingStation should create a station with EVSEs
+ const stationWithEvses = createChargingStation({
baseName: 'CS-EVSE-001',
- hasEvses: true,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
stationInfo: {
chargePointModel: 'EVSE Test Model',
chargePointVendor: 'EVSE Test Vendor',
ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
},
})
const evseComponents = reportData.filter(
(item: ReportDataType) => item.component.name === (OCPP20ComponentName.EVSE as string)
)
- if (stationWithEvses.evses.size > 0) {
+ if (stationWithEvses.hasEvses) {
expect(evseComponents.length).toBeGreaterThan(0)
}
})
OCPP20OptionalVariableName,
OCPP20RequiredVariableName,
OCPP20VendorVariableName,
+ OCPPVersion,
ReasonCodeEnumType,
} from '../../../../src/types/index.js'
import { Constants } from '../../../../src/utils/index.js'
-import { createChargingStationWithEvses } from '../../../ChargingStationFactory.js'
-import { TEST_CHARGING_STATION_NAME, TEST_CONNECTOR_VALID_INSTANCE } from './OCPP20TestConstants.js'
+import { createChargingStation } from '../../../ChargingStationFactory.js'
+import {
+ TEST_CHARGING_STATION_BASE_NAME,
+ TEST_CONNECTOR_VALID_INSTANCE,
+} from './OCPP20TestConstants.js'
import {
resetLimits,
resetReportingValueSize,
} from './OCPP20TestUtils.js'
void describe('B06 - Get Variables', () => {
- const mockChargingStation = createChargingStationWithEvses({
- baseName: TEST_CHARGING_STATION_NAME,
+ const mockChargingStation = createChargingStation({
+ 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,
})
--- /dev/null
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+/* eslint-disable @typescript-eslint/no-unsafe-assignment */
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import { expect } from '@std/expect'
+import { describe, it } from 'node:test'
+
+import type { OCPP20RequestStartTransactionRequest } from '../../../../src/types/index.js'
+
+import { OCPP20IncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.js'
+import { OCPPVersion, RequestStartStopStatusEnumType } from '../../../../src/types/index.js'
+import { OCPP20IdTokenEnumType } from '../../../../src/types/ocpp/2.0/Transaction.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { createChargingStation } from '../../../ChargingStationFactory.js'
+import { TEST_CHARGING_STATION_BASE_NAME } from './OCPP20TestConstants.js'
+import { resetLimits, resetReportingValueSize } from './OCPP20TestUtils.js'
+
+await describe('E01 - Remote Start Transaction', async () => {
+ const mockChargingStation = createChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ ocppRequestService: {
+ requestHandler: async () => {
+ // Mock successful OCPP request responses for StatusNotification and other requests
+ return Promise.resolve({})
+ },
+ },
+ stationInfo: {
+ ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+
+ const incomingRequestService = new OCPP20IncomingRequestService()
+
+ // Reset limits before each test
+ resetLimits(mockChargingStation)
+ resetReportingValueSize(mockChargingStation)
+
+ await it('Should handle RequestStartTransaction with valid evseId and idToken', async () => {
+ const validRequest: OCPP20RequestStartTransactionRequest = {
+ evseId: 1,
+ idToken: {
+ idToken: 'VALID_TOKEN_123',
+ type: OCPP20IdTokenEnumType.ISO14443,
+ },
+ remoteStartId: 1,
+ }
+
+ const response = await (incomingRequestService as any).handleRequestRequestStartTransaction(
+ mockChargingStation,
+ validRequest
+ )
+
+ expect(response).toBeDefined()
+ expect(response.status).toBe(RequestStartStopStatusEnumType.Accepted)
+ expect(response.transactionId).toBeDefined()
+ expect(typeof response.transactionId).toBe('string')
+ })
+
+ await it('Should handle RequestStartTransaction with remoteStartId', async () => {
+ const requestWithRemoteStartId: OCPP20RequestStartTransactionRequest = {
+ evseId: 2,
+ idToken: {
+ idToken: 'REMOTE_TOKEN_456',
+ type: OCPP20IdTokenEnumType.ISO15693,
+ },
+ remoteStartId: 42,
+ }
+
+ const response = await (incomingRequestService as any).handleRequestRequestStartTransaction(
+ mockChargingStation,
+ requestWithRemoteStartId
+ )
+
+ expect(response).toBeDefined()
+ expect(response.status).toBe(RequestStartStopStatusEnumType.Accepted)
+ expect(response.transactionId).toBeDefined()
+ })
+
+ await it('Should handle RequestStartTransaction with groupIdToken', async () => {
+ const requestWithGroupToken: OCPP20RequestStartTransactionRequest = {
+ evseId: 3,
+ groupIdToken: {
+ idToken: 'GROUP_TOKEN_789',
+ type: OCPP20IdTokenEnumType.Local,
+ },
+ idToken: {
+ idToken: 'PRIMARY_TOKEN',
+ type: OCPP20IdTokenEnumType.Central,
+ },
+ remoteStartId: 3,
+ }
+
+ const response = await (incomingRequestService as any).handleRequestRequestStartTransaction(
+ mockChargingStation,
+ requestWithGroupToken
+ )
+
+ expect(response).toBeDefined()
+ expect(response.status).toBe(RequestStartStopStatusEnumType.Accepted)
+ expect(response.transactionId).toBeDefined()
+ })
+
+ // TODO: Implement proper OCPP 2.0 ChargingProfile types and test charging profile functionality
+
+ await it('Should reject RequestStartTransaction for invalid evseId', async () => {
+ const invalidEvseRequest: OCPP20RequestStartTransactionRequest = {
+ evseId: 999, // Non-existent EVSE
+ idToken: {
+ idToken: 'VALID_TOKEN_123',
+ type: OCPP20IdTokenEnumType.ISO14443,
+ },
+ remoteStartId: 999,
+ }
+
+ // Should throw OCPPError for invalid evseId
+ await expect(
+ (incomingRequestService as any).handleRequestRequestStartTransaction(
+ mockChargingStation,
+ invalidEvseRequest
+ )
+ ).rejects.toThrow('EVSE 999 not found on charging station')
+ })
+
+ await it('Should reject RequestStartTransaction when connector is already occupied', async () => {
+ // First, start a transaction to occupy the connector
+ const firstRequest: OCPP20RequestStartTransactionRequest = {
+ evseId: 1,
+ idToken: {
+ idToken: 'FIRST_TOKEN',
+ type: OCPP20IdTokenEnumType.ISO14443,
+ },
+ remoteStartId: 100,
+ }
+
+ await (incomingRequestService as any).handleRequestRequestStartTransaction(
+ mockChargingStation,
+ firstRequest
+ )
+
+ // Now try to start another transaction on the same EVSE
+ const secondRequest: OCPP20RequestStartTransactionRequest = {
+ evseId: 1,
+ idToken: {
+ idToken: 'SECOND_TOKEN',
+ type: OCPP20IdTokenEnumType.ISO14443,
+ },
+ remoteStartId: 101,
+ }
+
+ const response = await (incomingRequestService as any).handleRequestRequestStartTransaction(
+ mockChargingStation,
+ secondRequest
+ )
+
+ expect(response).toBeDefined()
+ expect(response.status).toBe(RequestStartStopStatusEnumType.Rejected)
+ expect(response.transactionId).toBeDefined()
+ })
+
+ await it('Should return proper response structure', async () => {
+ const validRequest: OCPP20RequestStartTransactionRequest = {
+ evseId: 1,
+ idToken: {
+ idToken: 'STRUCTURE_TEST_TOKEN',
+ type: OCPP20IdTokenEnumType.ISO14443,
+ },
+ remoteStartId: 200,
+ }
+
+ const response = await (incomingRequestService as any).handleRequestRequestStartTransaction(
+ mockChargingStation,
+ validRequest
+ )
+
+ // Verify response structure
+ expect(response).toBeDefined()
+ expect(typeof response).toBe('object')
+ expect(response).toHaveProperty('status')
+ expect(response).toHaveProperty('transactionId')
+
+ // Verify status is valid enum value
+ expect(Object.values(RequestStartStopStatusEnumType)).toContain(response.status)
+
+ // Verify transactionId is a string (UUID format in OCPP 2.0)
+ expect(typeof response.transactionId).toBe('string')
+ expect(response.transactionId).toBeTruthy()
+ expect(response.transactionId?.length).toBeGreaterThan(0)
+ })
+})
import {
type OCPP20ResetRequest,
type OCPP20ResetResponse,
+ OCPPVersion,
ReasonCodeEnumType,
ResetEnumType,
ResetStatusEnumType,
} from '../../../../src/types/index.js'
import { Constants } from '../../../../src/utils/index.js'
-import { createChargingStationWithEvses } from '../../../ChargingStationFactory.js'
-import { TEST_CHARGING_STATION_NAME } from './OCPP20TestConstants.js'
+import { createChargingStation } from '../../../ChargingStationFactory.js'
+import { TEST_CHARGING_STATION_BASE_NAME } from './OCPP20TestConstants.js'
await describe('B11 & B12 - Reset', async () => {
- const mockChargingStation = createChargingStationWithEvses({
- baseName: TEST_CHARGING_STATION_NAME,
+ const mockChargingStation = createChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
stationInfo: {
ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
resetTime: 5000,
},
websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
type OCPP20SetVariableResultType,
type OCPP20SetVariablesRequest,
OCPP20VendorVariableName,
+ OCPPVersion,
ReasonCodeEnumType,
SetVariableStatusEnumType,
} from '../../../../src/types/index.js'
import { Constants } from '../../../../src/utils/index.js'
-import { createChargingStationWithEvses } from '../../../ChargingStationFactory.js'
-import { TEST_CHARGING_STATION_NAME, TEST_CONNECTOR_VALID_INSTANCE } from './OCPP20TestConstants.js'
+import { createChargingStation } from '../../../ChargingStationFactory.js'
+import {
+ TEST_CHARGING_STATION_BASE_NAME,
+ TEST_CONNECTOR_VALID_INSTANCE,
+} from './OCPP20TestConstants.js'
import {
resetLimits,
resetValueSizeLimits,
/* eslint-disable @typescript-eslint/no-floating-promises */
describe('B07 - Set Variables', () => {
- const mockChargingStation = createChargingStationWithEvses({
- baseName: TEST_CHARGING_STATION_NAME,
+ const mockChargingStation = createChargingStation({
+ 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,
})
BootReasonEnumType,
type OCPP20BootNotificationRequest,
OCPP20RequestCommand,
+ OCPPVersion,
} from '../../../../src/types/index.js'
import { type ChargingStationType } from '../../../../src/types/ocpp/2.0/Common.js'
import { Constants } from '../../../../src/utils/index.js'
TEST_CHARGE_POINT_MODEL,
TEST_CHARGE_POINT_SERIAL_NUMBER,
TEST_CHARGE_POINT_VENDOR,
- TEST_CHARGING_STATION_NAME,
+ TEST_CHARGING_STATION_BASE_NAME,
TEST_FIRMWARE_VERSION,
} from './OCPP20TestConstants.js'
const requestService = new OCPP20RequestService(mockResponseService)
const mockChargingStation = createChargingStation({
- baseName: TEST_CHARGING_STATION_NAME,
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
stationInfo: {
chargePointModel: TEST_CHARGE_POINT_MODEL,
chargePointVendor: TEST_CHARGE_POINT_VENDOR,
firmwareVersion: TEST_FIRMWARE_VERSION,
ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
},
websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
})
import { OCPP20RequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestService.js'
import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js'
-import { type OCPP20HeartbeatRequest, OCPP20RequestCommand } from '../../../../src/types/index.js'
+import {
+ type OCPP20HeartbeatRequest,
+ OCPP20RequestCommand,
+ OCPPVersion,
+} from '../../../../src/types/index.js'
import { Constants, has } from '../../../../src/utils/index.js'
import { createChargingStation } from '../../../ChargingStationFactory.js'
import {
TEST_CHARGE_POINT_MODEL,
TEST_CHARGE_POINT_SERIAL_NUMBER,
TEST_CHARGE_POINT_VENDOR,
- TEST_CHARGING_STATION_NAME,
+ TEST_CHARGING_STATION_BASE_NAME,
TEST_FIRMWARE_VERSION,
} from './OCPP20TestConstants.js'
const requestService = new OCPP20RequestService(mockResponseService)
const mockChargingStation = createChargingStation({
- baseName: TEST_CHARGING_STATION_NAME,
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
stationInfo: {
chargePointModel: TEST_CHARGE_POINT_MODEL,
chargePointVendor: TEST_CHARGE_POINT_VENDOR,
firmwareVersion: TEST_FIRMWARE_VERSION,
ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
},
websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
})
await it('Should handle HeartBeat request with different charging station configurations', () => {
const alternativeChargingStation = createChargingStation({
baseName: 'CS-ALTERNATIVE-002',
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
heartbeatInterval: 120,
stationInfo: {
chargePointModel: 'Alternative Model',
chargePointVendor: 'Alternative Vendor',
firmwareVersion: '2.5.1',
ocppStrictCompliance: true,
+ ocppVersion: OCPPVersion.VERSION_201,
},
websocketPingInterval: 45,
})
type OCPP20NotifyReportRequest,
OCPP20OptionalVariableName,
OCPP20RequestCommand,
+ OCPPVersion,
type ReportDataType,
} from '../../../../src/types/index.js'
import { Constants } from '../../../../src/utils/index.js'
TEST_CHARGE_POINT_MODEL,
TEST_CHARGE_POINT_SERIAL_NUMBER,
TEST_CHARGE_POINT_VENDOR,
- TEST_CHARGING_STATION_NAME,
+ TEST_CHARGING_STATION_BASE_NAME,
TEST_FIRMWARE_VERSION,
} from './OCPP20TestConstants.js'
const requestService = new OCPP20RequestService(mockResponseService)
const mockChargingStation = createChargingStation({
- baseName: TEST_CHARGING_STATION_NAME,
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
stationInfo: {
chargePointModel: TEST_CHARGE_POINT_MODEL,
chargePointVendor: TEST_CHARGE_POINT_VENDOR,
firmwareVersion: TEST_FIRMWARE_VERSION,
ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
},
websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
})
OCPP20ConnectorStatusEnumType,
OCPP20RequestCommand,
type OCPP20StatusNotificationRequest,
+ OCPPVersion,
} from '../../../../src/types/index.js'
import { Constants } from '../../../../src/utils/index.js'
-import { createChargingStationWithEvses } from '../../../ChargingStationFactory.js'
+import { createChargingStation } from '../../../ChargingStationFactory.js'
import {
TEST_FIRMWARE_VERSION,
TEST_STATUS_CHARGE_POINT_MODEL,
TEST_STATUS_CHARGE_POINT_SERIAL_NUMBER,
TEST_STATUS_CHARGE_POINT_VENDOR,
- TEST_STATUS_CHARGING_STATION_NAME,
+ TEST_STATUS_CHARGING_STATION_BASE_NAME,
} from './OCPP20TestConstants.js'
await describe('G01 - Status Notification', async () => {
const mockResponseService = new OCPP20ResponseService()
const requestService = new OCPP20RequestService(mockResponseService)
- const mockChargingStation = createChargingStationWithEvses({
- baseName: TEST_STATUS_CHARGING_STATION_NAME,
+ const mockChargingStation = createChargingStation({
+ baseName: TEST_STATUS_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
stationInfo: {
chargePointModel: TEST_STATUS_CHARGE_POINT_MODEL,
chargePointVendor: TEST_STATUS_CHARGE_POINT_VENDOR,
firmwareVersion: TEST_FIRMWARE_VERSION,
ocppStrictCompliance: false,
+ ocppVersion: OCPPVersion.VERSION_201,
},
websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
})
*/
// Test charging station information
-export const TEST_CHARGING_STATION_NAME = 'CS-TEST-001'
+export const TEST_CHARGING_STATION_BASE_NAME = 'CS-TEST'
export const TEST_CHARGE_POINT_MODEL = 'Test Model'
export const TEST_CHARGE_POINT_SERIAL_NUMBER = 'TEST-SN-001'
export const TEST_CHARGE_POINT_VENDOR = 'Test Vendor'
export const TEST_CONNECTOR_INVALID_INSTANCE = '999'
// Test charging station information for status notification tests
-export const TEST_STATUS_CHARGING_STATION_NAME = 'CS-TEST-STATUS-001'
+export const TEST_STATUS_CHARGING_STATION_BASE_NAME = 'CS-TEST-STATUS'
export const TEST_STATUS_CHARGE_POINT_MODEL = 'Test Status Model'
export const TEST_STATUS_CHARGE_POINT_SERIAL_NUMBER = 'TEST-STATUS-SN-001'
export const TEST_STATUS_CHARGE_POINT_VENDOR = 'Test Status Vendor'
OCPP20RequiredVariableName,
type OCPP20SetVariableDataType,
OCPP20VendorVariableName,
+ OCPPVersion,
ReasonCodeEnumType,
SetVariableStatusEnumType,
type VariableType,
} from '../../../../src/types/index.js'
import { Constants } from '../../../../src/utils/index.js'
-import {
- createChargingStation,
- createChargingStationWithEvses,
-} from '../../../ChargingStationFactory.js'
-import { TEST_CHARGING_STATION_NAME } from './OCPP20TestConstants.js'
+import { createChargingStation } from '../../../ChargingStationFactory.js'
+import { TEST_CHARGING_STATION_BASE_NAME } from './OCPP20TestConstants.js'
import {
resetReportingValueSize,
resetValueSizeLimits,
await describe('OCPP20VariableManager test suite', async () => {
// Create mock ChargingStation with EVSEs for OCPP 2.0 testing
- const mockChargingStation = createChargingStationWithEvses({
- baseName: TEST_CHARGING_STATION_NAME,
+ const mockChargingStation = createChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
})
const manager = OCPP20VariableManager.getInstance()
const station = createChargingStation({
baseName: 'MMStation',
+ connectorsCount: 3,
+ evseConfiguration: { evsesCount: 3 },
heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ ocppVersion: OCPPVersion.VERSION_201,
+ },
websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
})