From: Jérôme Benoit Date: Mon, 3 Nov 2025 16:44:04 +0000 (+0100) Subject: feat(ocpp2): add RequestStartTransaction command (#1583) X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=e5f4eca2842c1858a3753df16bfc55727753a6f8;p=e-mobility-charging-stations-simulator.git feat(ocpp2): add RequestStartTransaction command (#1583) * feat(ocpp2): add RequestStartTransaction command Signed-off-by: Jérôme Benoit * chore: refine opencode configuration Signed-off-by: Jérôme Benoit * chore: refine opencode configuration Signed-off-by: Jérôme Benoit * refactor: cleanups OCPP2 type definition Signed-off-by: Jérôme Benoit * fix: ocpp2 type definition Signed-off-by: Jérôme Benoit * refactor: standardize OCPP2 unit references using enum constants - Replace hardcoded unit strings with OCPP20UnitEnumType enum references in Variable Registry - Add CHARS custom extension to OCPP20UnitEnumType for non-standard character count units - Update OCPP20UnitOfMeasure interface to use enum type for better type safety - Improves type safety and maintains single source of truth for OCPP2 units * refactor: move OCPP20UnitEnumType enum to Common.ts for better architectural organization * chore: cleanup meter values types integration Signed-off-by: Jérôme Benoit * refactor: cleanup OCPP stack error messages Signed-off-by: Jérôme Benoit * refactor: use enums in variables registry Signed-off-by: Jérôme Benoit * Update tests/ChargingStationFactory.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: proper type definition for SampledValueTemplate Signed-off-by: Jérôme Benoit * refactor: code formatting Signed-off-by: Jérôme Benoit * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * test: refine request transaction payload Signed-off-by: Jérôme Benoit * refactor: handle remoteStartId Signed-off-by: Jérôme Benoit * docs: update README.md Signed-off-by: Jérôme Benoit --------- Signed-off-by: Jérôme Benoit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- diff --git a/.opencode/agent/review.md b/.opencode/agent/review.md new file mode 100644 index 00000000..96f8b88a --- /dev/null +++ b/.opencode/agent/review.md @@ -0,0 +1,21 @@ +--- +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. diff --git a/.opencode/command/format-simulator.md b/.opencode/command/format-simulator.md new file mode 100644 index 00000000..19f0eb35 --- /dev/null +++ b/.opencode/command/format-simulator.md @@ -0,0 +1,8 @@ +--- +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. diff --git a/.opencode/command/tests-simulator.md b/.opencode/command/test-simulator.md similarity index 57% rename from .opencode/command/tests-simulator.md rename to .opencode/command/test-simulator.md index 0621de91..60a15fde 100644 --- a/.opencode/command/tests-simulator.md +++ b/.opencode/command/test-simulator.md @@ -1,8 +1,8 @@ --- -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. diff --git a/.opencode/command/tests-simulator-file.md b/.opencode/command/tests-simulator-file.md deleted file mode 100644 index a0164569..00000000 --- a/.opencode/command/tests-simulator-file.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -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. diff --git a/.serena/memories/code_style_conventions.md b/.serena/memories/code_style_conventions.md index 9d5f7498..55b2fb87 100644 --- a/.serena/memories/code_style_conventions.md +++ b/.serena/memories/code_style_conventions.md @@ -21,7 +21,7 @@ - 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 diff --git a/README.md b/README.md index b1e3ae86..a212f348 100644 --- a/README.md +++ b/README.md @@ -511,7 +511,7 @@ make SUBMODULES_INIT=true #### E. Transactions -- :x: RequestStartTransaction +- :white_check_mark: RequestStartTransaction - :x: RequestStopTransaction - :x: TransactionEvent diff --git a/eslint.config.js b/eslint.config.js index 6af2482e..72c72c40 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -51,9 +51,8 @@ export default defineConfig([ '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', @@ -69,6 +68,10 @@ export default defineConfig([ 'DEAUTHORIZE', 'deauthorized', 'DEAUTHORIZED', + 'Selftest', + 'SECC', + 'Secc', + 'Overcurrent', ], }, }, diff --git a/src/charging-station/Helpers.ts b/src/charging-station/Helpers.ts index 01eee0fc..c1edc530 100644 --- a/src/charging-station/Helpers.ts +++ b/src/charging-station/Helpers.ts @@ -238,7 +238,7 @@ export const validateStationInfo = (chargingStation: ChargingStation): void => { 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` ) } } @@ -517,6 +517,7 @@ export const prepareConnectorStatus = (connectorStatus: ConnectorStatus): Connec ) .map(chargingProfile => { chargingProfile.chargingSchedule.startSchedule = convertToDate( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument chargingProfile.chargingSchedule.startSchedule ) chargingProfile.validFrom = convertToDate(chargingProfile.validFrom) @@ -719,6 +720,7 @@ export const getChargingStationChargingProfilesLimit = ( 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 ) @@ -784,6 +786,7 @@ export const getConnectorChargingProfilesLimit = ( 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 ) @@ -992,6 +995,7 @@ const getChargingProfilesLimit = ( 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 @@ -999,16 +1003,19 @@ const getChargingProfilesLimit = ( } 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 ( @@ -1027,7 +1034,9 @@ const getChargingProfilesLimit = ( // 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, }) ) { @@ -1038,26 +1047,33 @@ const getChargingProfilesLimit = ( ): number => a.startPeriod - b.startPeriod if ( !isArraySorted( + // 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) @@ -1068,10 +1084,12 @@ const getChargingProfilesLimit = ( 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 ) @@ -1079,6 +1097,7 @@ const getChargingProfilesLimit = ( // 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) @@ -1086,24 +1105,30 @@ const getChargingProfilesLimit = ( } // 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 } } @@ -1152,6 +1177,7 @@ export const canProceedChargingProfile = ( (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() }` @@ -1163,18 +1189,22 @@ export const canProceedChargingProfile = ( 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 @@ -1225,9 +1255,9 @@ const prepareRecurringChargingProfile = ( 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) @@ -1235,12 +1265,14 @@ const prepareRecurringChargingProfile = ( !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 @@ -1248,9 +1280,9 @@ const prepareRecurringChargingProfile = ( 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) @@ -1258,12 +1290,14 @@ const prepareRecurringChargingProfile = ( !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 @@ -1271,7 +1305,7 @@ const prepareRecurringChargingProfile = ( 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` ) } @@ -1281,6 +1315,7 @@ const prepareRecurringChargingProfile = ( `${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( @@ -1302,6 +1337,7 @@ const checkRecurringChargingProfileDuration = ( 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 @@ -1314,6 +1350,7 @@ const checkRecurringChargingProfileDuration = ( 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 diff --git a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts index 0d55122b..621dc14f 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts @@ -61,6 +61,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { 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), diff --git a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts index a1269b1d..60524365 100644 --- a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts @@ -3,6 +3,10 @@ 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 { @@ -27,6 +31,8 @@ import { type OCPP20NotifyReportRequest, type OCPP20NotifyReportResponse, OCPP20RequestCommand, + type OCPP20RequestStartTransactionRequest, + type OCPP20RequestStartTransactionResponse, OCPP20RequiredVariableName, type OCPP20ResetRequest, type OCPP20ResetResponse, @@ -36,15 +42,17 @@ import { 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' @@ -80,6 +88,10 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { 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, @@ -658,7 +670,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } // 4. EVSE and connector information - if (chargingStation.evses.size > 0) { + if (chargingStation.hasEvses) { for (const [evseId, evse] of chargingStation.evses) { reportData.push({ component: { @@ -693,7 +705,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } } } - } else if (chargingStation.connectors.size > 0) { + } else { // Fallback to connectors if no EVSE structure for (const [connectorId, connector] of chargingStation.connectors) { if (connectorId > 0) { @@ -768,7 +780,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { variableCharacteristics: { dataType: DataEnumType.string, supportsMonitoring: true }, }) - if (chargingStation.evses.size > 0) { + if (chargingStation.hasEvses) { for (const [evseId, evse] of chargingStation.evses) { reportData.push({ component: { @@ -782,7 +794,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { 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) { @@ -855,6 +867,221 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } } + private async handleRequestRequestStartTransaction ( + chargingStation: ChargingStation, + commandPayload: OCPP20RequestStartTransactionRequest + ): Promise { + 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 @@ -1053,6 +1280,27 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } } + // Helper methods for RequestStartTransaction + private async isIdTokenAuthorized ( + chargingStation: ChargingStation, + idToken: OCPP20IdTokenType + ): Promise { + // 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, @@ -1181,6 +1429,26 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { 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, diff --git a/src/charging-station/ocpp/2.0/OCPP20VariableRegistry.ts b/src/charging-station/ocpp/2.0/OCPP20VariableRegistry.ts index 934a4267..c536e621 100644 --- a/src/charging-station/ocpp/2.0/OCPP20VariableRegistry.ts +++ b/src/charging-station/ocpp/2.0/OCPP20VariableRegistry.ts @@ -11,9 +11,11 @@ import { OCPP20MeasurandEnumType, OCPP20OptionalVariableName, OCPP20RequiredVariableName, + OCPP20UnitEnumType, OCPP20VendorVariableName, PersistenceEnumType, ReasonCodeEnumType, + type VariableName, } from '../../../types/index.js' import { Constants, convertToIntOrNaN, has } from '../../../utils/index.js' @@ -67,9 +69,9 @@ export interface VariableMetadata { rebootRequired?: boolean supportedAttributes: AttributeEnumType[] supportsTarget?: boolean - unit?: string + unit?: OCPP20UnitEnumType urlSchemes?: string[] - variable: string + variable: VariableName vendorSpecific?: boolean } @@ -130,7 +132,7 @@ export const VARIABLE_REGISTRY: Record = { mutability: MutabilityEnumType.ReadWrite, persistence: PersistenceEnumType.Persistent, supportedAttributes: [AttributeEnumType.Actual], - unit: 's', + unit: OCPP20UnitEnumType.SECONDS, variable: 'Interval', }, [buildRegistryKey(OCPP20ComponentName.AlignedDataCtrlr as string, 'Measurands')]: { @@ -198,7 +200,7 @@ export const VARIABLE_REGISTRY: Record = { mutability: MutabilityEnumType.ReadWrite, persistence: PersistenceEnumType.Persistent, supportedAttributes: [AttributeEnumType.Actual], - unit: 's', + unit: OCPP20UnitEnumType.SECONDS, variable: 'TxEndedInterval', }, [buildRegistryKey(OCPP20ComponentName.AlignedDataCtrlr as string, 'TxEndedMeasurands')]: { @@ -232,7 +234,7 @@ export const VARIABLE_REGISTRY: Record = { mutability: MutabilityEnumType.ReadWrite, persistence: PersistenceEnumType.Persistent, supportedAttributes: [AttributeEnumType.Actual], - variable: 'TxEndedMeasurands', + variable: OCPP20RequiredVariableName.TxEndedMeasurands, }, // AuthCacheCtrlr Component @@ -304,7 +306,7 @@ export const VARIABLE_REGISTRY: Record = { mutability: MutabilityEnumType.ReadOnly, persistence: PersistenceEnumType.Volatile, supportedAttributes: [AttributeEnumType.Actual, AttributeEnumType.MaxSet], - unit: 'B', + unit: OCPP20UnitEnumType.BYTES, variable: 'Storage', }, @@ -512,7 +514,7 @@ export const VARIABLE_REGISTRY: Record = { 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, @@ -534,7 +536,7 @@ export const VARIABLE_REGISTRY: Record = { mutability: MutabilityEnumType.ReadOnly, persistence: PersistenceEnumType.Persistent, supportedAttributes: [AttributeEnumType.Actual], - variable: 'VendorName', + variable: OCPP20DeviceInfoVariableName.VendorName, }, [buildRegistryKey( OCPP20ComponentName.ChargingStation as string, @@ -565,7 +567,7 @@ export const VARIABLE_REGISTRY: Record = { mutability: MutabilityEnumType.ReadWrite, persistence: PersistenceEnumType.Persistent, supportedAttributes: [AttributeEnumType.Actual], - unit: 's', + unit: OCPP20UnitEnumType.SECONDS, variable: OCPP20OptionalVariableName.WebSocketPingInterval as string, }, [buildRegistryKey( @@ -766,7 +768,7 @@ export const VARIABLE_REGISTRY: Record = { persistence: PersistenceEnumType.Persistent, positive: true, supportedAttributes: [AttributeEnumType.Actual], - unit: 'chars', + unit: OCPP20UnitEnumType.CHARS, variable: OCPP20RequiredVariableName.ConfigurationValueSize as string, }, [buildRegistryKey( @@ -838,7 +840,7 @@ export const VARIABLE_REGISTRY: Record = { persistence: PersistenceEnumType.Persistent, positive: true, supportedAttributes: [AttributeEnumType.Actual], - unit: 'chars', + unit: OCPP20UnitEnumType.CHARS, variable: OCPP20RequiredVariableName.ReportingValueSize as string, }, [buildRegistryKey( @@ -856,7 +858,7 @@ export const VARIABLE_REGISTRY: Record = { persistence: PersistenceEnumType.Persistent, positive: true, supportedAttributes: [AttributeEnumType.Actual], - unit: 'chars', + unit: OCPP20UnitEnumType.CHARS, variable: OCPP20RequiredVariableName.ValueSize as string, }, @@ -880,7 +882,7 @@ export const VARIABLE_REGISTRY: Record = { 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, @@ -926,7 +928,7 @@ export const VARIABLE_REGISTRY: Record = { persistence: PersistenceEnumType.Volatile, supportedAttributes: [AttributeEnumType.Actual, AttributeEnumType.MaxSet], supportsTarget: false, - unit: 'W', + unit: OCPP20UnitEnumType.WATT, variable: 'Power', }, [buildRegistryKey(OCPP20ComponentName.EVSE as string, 'SupplyPhases')]: { @@ -1015,7 +1017,7 @@ export const VARIABLE_REGISTRY: Record = { 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, @@ -1101,7 +1103,7 @@ export const VARIABLE_REGISTRY: Record = { 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, @@ -1145,7 +1147,7 @@ export const VARIABLE_REGISTRY: Record = { mutability: MutabilityEnumType.ReadOnly, persistence: PersistenceEnumType.Persistent, supportedAttributes: [AttributeEnumType.Actual], - variable: 'ItemsPerMessage', + variable: OCPP20RequiredVariableName.ItemsPerMessage, }, [buildRegistryKey(OCPP20ComponentName.LocalAuthListCtrlr as string, 'Storage')]: { characteristics: { @@ -1160,7 +1162,7 @@ export const VARIABLE_REGISTRY: Record = { mutability: MutabilityEnumType.ReadOnly, persistence: PersistenceEnumType.Volatile, supportedAttributes: [AttributeEnumType.Actual, AttributeEnumType.MaxSet], - unit: 'B', + unit: OCPP20UnitEnumType.BYTES, variable: 'Storage', }, @@ -1214,7 +1216,7 @@ export const VARIABLE_REGISTRY: Record = { mutability: MutabilityEnumType.ReadOnly, persistence: PersistenceEnumType.Persistent, supportedAttributes: [AttributeEnumType.Actual], - variable: 'BytesPerMessage', + variable: OCPP20RequiredVariableName.BytesPerMessage, }, [buildRegistryKey( OCPP20ComponentName.MonitoringCtrlr as string, @@ -1230,7 +1232,7 @@ export const VARIABLE_REGISTRY: Record = { 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, @@ -1256,7 +1258,7 @@ export const VARIABLE_REGISTRY: Record = { mutability: MutabilityEnumType.ReadOnly, persistence: PersistenceEnumType.Persistent, supportedAttributes: [AttributeEnumType.Actual], - variable: 'ItemsPerMessage', + variable: OCPP20RequiredVariableName.ItemsPerMessage, }, [buildRegistryKey( OCPP20ComponentName.MonitoringCtrlr as string, @@ -1273,7 +1275,7 @@ export const VARIABLE_REGISTRY: Record = { 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, @@ -1400,7 +1402,7 @@ export const VARIABLE_REGISTRY: Record = { persistence: PersistenceEnumType.Persistent, positive: true, supportedAttributes: [AttributeEnumType.Actual], - unit: 's', + unit: OCPP20UnitEnumType.SECONDS, variable: OCPP20OptionalVariableName.HeartbeatInterval as string, }, [buildRegistryKey( @@ -1417,7 +1419,7 @@ export const VARIABLE_REGISTRY: Record = { mutability: MutabilityEnumType.ReadWrite, persistence: PersistenceEnumType.Persistent, supportedAttributes: [AttributeEnumType.Actual], - unit: 's', + unit: OCPP20UnitEnumType.SECONDS, variable: OCPP20OptionalVariableName.WebSocketPingInterval as string, }, [buildRegistryKey( @@ -1450,7 +1452,7 @@ export const VARIABLE_REGISTRY: Record = { persistence: PersistenceEnumType.Persistent, positive: true, supportedAttributes: [AttributeEnumType.Actual], - unit: 's', + unit: OCPP20UnitEnumType.SECONDS, variable: OCPP20RequiredVariableName.MessageAttemptInterval as string, }, [buildRegistryKey( @@ -1487,7 +1489,7 @@ export const VARIABLE_REGISTRY: Record = { persistence: PersistenceEnumType.Persistent, positive: true, supportedAttributes: [AttributeEnumType.Actual], - unit: 's', + unit: OCPP20UnitEnumType.SECONDS, variable: OCPP20RequiredVariableName.MessageTimeout as string, }, [buildRegistryKey( @@ -1533,7 +1535,7 @@ export const VARIABLE_REGISTRY: Record = { persistence: PersistenceEnumType.Persistent, positive: true, supportedAttributes: [AttributeEnumType.Actual], - unit: 's', + unit: OCPP20UnitEnumType.SECONDS, variable: OCPP20RequiredVariableName.OfflineThreshold as string, }, [buildRegistryKey( @@ -1653,7 +1655,7 @@ export const VARIABLE_REGISTRY: Record = { mutability: MutabilityEnumType.ReadWrite, persistence: PersistenceEnumType.Persistent, supportedAttributes: [AttributeEnumType.Actual], - unit: 's', + unit: OCPP20UnitEnumType.SECONDS, variable: 'TxEndedInterval', }, [buildRegistryKey( @@ -1667,7 +1669,7 @@ export const VARIABLE_REGISTRY: Record = { mutability: MutabilityEnumType.ReadOnly, persistence: PersistenceEnumType.Volatile, supportedAttributes: [AttributeEnumType.Actual], - unit: 'A', + unit: OCPP20UnitEnumType.AMP, variable: OCPP20MeasurandEnumType.CURRENT_IMPORT, }, [buildRegistryKey( @@ -1681,7 +1683,7 @@ export const VARIABLE_REGISTRY: Record = { mutability: MutabilityEnumType.ReadOnly, persistence: PersistenceEnumType.Volatile, supportedAttributes: [AttributeEnumType.Actual], - unit: 'Wh', + unit: OCPP20UnitEnumType.WATT_HOUR, variable: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER, }, [buildRegistryKey( @@ -1695,7 +1697,7 @@ export const VARIABLE_REGISTRY: Record = { mutability: MutabilityEnumType.ReadOnly, persistence: PersistenceEnumType.Volatile, supportedAttributes: [AttributeEnumType.Actual], - unit: 'W', + unit: OCPP20UnitEnumType.WATT, variable: OCPP20MeasurandEnumType.POWER_ACTIVE_IMPORT, }, [buildRegistryKey( @@ -1709,7 +1711,7 @@ export const VARIABLE_REGISTRY: Record = { mutability: MutabilityEnumType.ReadOnly, persistence: PersistenceEnumType.Volatile, supportedAttributes: [AttributeEnumType.Actual], - unit: 'V', + unit: OCPP20UnitEnumType.VOLT, variable: OCPP20MeasurandEnumType.VOLTAGE, }, [buildRegistryKey( @@ -1787,7 +1789,7 @@ export const VARIABLE_REGISTRY: Record = { persistence: PersistenceEnumType.Volatile, positive: true, supportedAttributes: [AttributeEnumType.Actual], - unit: 's', + unit: OCPP20UnitEnumType.SECONDS, variable: OCPP20RequiredVariableName.TxUpdatedInterval as string, }, [buildRegistryKey( @@ -1863,7 +1865,7 @@ export const VARIABLE_REGISTRY: Record = { mutability: MutabilityEnumType.ReadWrite, persistence: PersistenceEnumType.Persistent, supportedAttributes: [AttributeEnumType.Actual], - unit: 's', + unit: OCPP20UnitEnumType.SECONDS, variable: 'CertSigningWaitMinimum', }, [buildRegistryKey(OCPP20ComponentName.SecurityCtrlr as string, 'Identity')]: { @@ -2027,7 +2029,7 @@ export const VARIABLE_REGISTRY: Record = { mutability: MutabilityEnumType.ReadWrite, persistence: PersistenceEnumType.Persistent, supportedAttributes: [AttributeEnumType.Actual], - unit: 'Percent', + unit: OCPP20UnitEnumType.PERCENT, variable: 'LimitChangeSignificance', }, [buildRegistryKey( @@ -2180,7 +2182,7 @@ export const VARIABLE_REGISTRY: Record = { mutability: MutabilityEnumType.ReadOnly, persistence: PersistenceEnumType.Volatile, supportedAttributes: [AttributeEnumType.Actual], - unit: 's', + unit: OCPP20UnitEnumType.SECONDS, variable: 'ChargingTime', }, [buildRegistryKey(OCPP20ComponentName.TxCtrlr as string, 'MaxEnergyOnInvalidId')]: { @@ -2192,7 +2194,7 @@ export const VARIABLE_REGISTRY: Record = { mutability: MutabilityEnumType.ReadWrite, persistence: PersistenceEnumType.Persistent, supportedAttributes: [AttributeEnumType.Actual], - unit: 'Wh', + unit: OCPP20UnitEnumType.WATT_HOUR, variable: 'MaxEnergyOnInvalidId', }, [buildRegistryKey(OCPP20ComponentName.TxCtrlr as string, 'TxBeforeAcceptedEnabled')]: { @@ -2221,7 +2223,7 @@ export const VARIABLE_REGISTRY: Record = { persistence: PersistenceEnumType.Persistent, positive: true, supportedAttributes: [AttributeEnumType.Actual], - unit: 's', + unit: OCPP20UnitEnumType.SECONDS, variable: OCPP20RequiredVariableName.EVConnectionTimeOut as string, }, [buildRegistryKey( diff --git a/src/charging-station/ocpp/OCPPServiceUtils.ts b/src/charging-station/ocpp/OCPPServiceUtils.ts index 36d66fcb..83288e9b 100644 --- a/src/charging-station/ocpp/OCPPServiceUtils.ts +++ b/src/charging-station/ocpp/OCPPServiceUtils.ts @@ -36,8 +36,11 @@ import { MeterValuePhase, MeterValueUnit, type OCPP16ChargePointStatus, + type OCPP16SampledValue, type OCPP16StatusNotificationRequest, type OCPP20ConnectorStatusEnumType, + type OCPP20MeterValue, + type OCPP20SampledValue, type OCPP20StatusNotificationRequest, OCPPVersion, RequestCommand, @@ -104,7 +107,12 @@ const buildStatusNotificationRequest = ( 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 + ) } } @@ -251,9 +259,11 @@ const checkConnectorStatusTransition = ( } 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) { @@ -355,7 +365,11 @@ export const buildMeterValue = ( ) : randomInt(socMinimumValue, socMaximumValue + 1) meterValue.sampledValue.push( - buildSampledValue(socSampledValueTemplate, socSampledValueTemplateValue) + buildSampledValue( + chargingStation.stationInfo.ocppVersion, + socSampledValueTemplate, + socSampledValueTemplateValue + ) ) const sampledValuesIndex = meterValue.sampledValue.length - 1 if ( @@ -368,9 +382,9 @@ export const buildMeterValue = ( 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()}` ) } } @@ -397,7 +411,11 @@ export const buildMeterValue = ( chargingStation.stationInfo.mainVoltageMeterValues === true) ) { meterValue.sampledValue.push( - buildSampledValue(voltageSampledValueTemplate, voltageMeasurandValue) + buildSampledValue( + chargingStation.stationInfo.ocppVersion, + voltageSampledValueTemplate, + voltageMeasurandValue + ) ) } for ( @@ -430,6 +448,7 @@ export const buildMeterValue = ( } meterValue.sampledValue.push( buildSampledValue( + chargingStation.stationInfo.ocppVersion, voltagePhaseLineToNeutralSampledValueTemplate ?? voltageSampledValueTemplate, voltagePhaseLineToNeutralMeasurandValue ?? voltageMeasurandValue, undefined, @@ -475,6 +494,7 @@ export const buildMeterValue = ( ) meterValue.sampledValue.push( buildSampledValue( + chargingStation.stationInfo.ocppVersion, voltagePhaseLineToLineSampledValueTemplate ?? voltageSampledValueTemplate, voltagePhaseLineToLineMeasurandValue ?? defaultVoltagePhaseLineToLineMeasurandValue, undefined, @@ -684,7 +704,11 @@ export const buildMeterValue = ( 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) @@ -701,9 +725,9 @@ export const buildMeterValue = ( 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 ( @@ -714,6 +738,7 @@ export const buildMeterValue = ( const phaseValue = `L${phase.toString()}-N` meterValue.sampledValue.push( buildSampledValue( + chargingStation.stationInfo.ocppVersion, powerPerPhaseSampledValueTemplates[ `L${phase.toString()}` as keyof MeasurandPerPhaseSampledValueTemplates ] ?? powerSampledValueTemplate, @@ -748,9 +773,9 @@ export const buildMeterValue = ( // 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()}` ) } } @@ -946,7 +971,11 @@ export const buildMeterValue = ( 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 ( @@ -961,9 +990,9 @@ export const buildMeterValue = ( 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 ( @@ -974,6 +1003,7 @@ export const buildMeterValue = ( const phaseValue = `L${phase.toString()}` meterValue.sampledValue.push( buildSampledValue( + chargingStation.stationInfo.ocppVersion, currentPerPhaseSampledValueTemplates[ phaseValue as keyof MeasurandPerPhaseSampledValueTemplates ] ?? currentSampledValueTemplate, @@ -998,9 +1028,9 @@ export const buildMeterValue = ( // 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()}` ) } } @@ -1037,7 +1067,6 @@ export const buildMeterValue = ( energySampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) : getRandomFloatRounded(connectorMaximumEnergyRounded, connectorMinimumEnergyRounded) - // Persist previous value on connector if (connector != null) { if ( connector.energyActiveImportRegisterValue != null && @@ -1054,6 +1083,7 @@ export const buildMeterValue = ( } meterValue.sampledValue.push( buildSampledValue( + chargingStation.stationInfo.ocppVersion, energySampledValueTemplate, roundTo( chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) / @@ -1079,11 +1109,246 @@ export const buildMeterValue = ( } 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 ) } } @@ -1098,6 +1363,8 @@ export const buildTransactionEndMeterValue = ( let unitDivider: number switch (chargingStation.stationInfo?.ocppVersion) { case OCPPVersion.VERSION_16: + case OCPPVersion.VERSION_20: + case OCPPVersion.VERSION_201: meterValue = { sampledValue: [], timestamp: new Date(), @@ -1107,6 +1374,7 @@ export const buildTransactionEndMeterValue = ( 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), @@ -1114,12 +1382,12 @@ export const buildTransactionEndMeterValue = ( ) ) 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 ) } } @@ -1179,6 +1447,11 @@ const getLimitFromSampledValueTemplateCustomValue = ( ) } +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, @@ -1186,7 +1459,7 @@ const getSampledValueTemplate = ( 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()}` ) @@ -1215,7 +1488,7 @@ const getSampledValueTemplate = ( index++ ) { if ( - !OCPPConstants.OCPP_MEASURANDS_SUPPORTED.includes( + !isMeasurandSupported( sampledValueTemplates[index].measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER ) ) { @@ -1260,29 +1533,79 @@ const getSampledValueTemplate = ( ) } -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 = ( diff --git a/src/types/ConnectorStatus.ts b/src/types/ConnectorStatus.ts index d77a8744..b3ebfe15 100644 --- a/src/types/ConnectorStatus.ts +++ b/src/types/ConnectorStatus.ts @@ -16,11 +16,12 @@ export interface ConnectorStatus { 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 diff --git a/src/types/MeasurandPerPhaseSampledValueTemplates.ts b/src/types/MeasurandPerPhaseSampledValueTemplates.ts index f35f4f64..7c262192 100644 --- a/src/types/MeasurandPerPhaseSampledValueTemplates.ts +++ b/src/types/MeasurandPerPhaseSampledValueTemplates.ts @@ -6,7 +6,7 @@ export interface MeasurandPerPhaseSampledValueTemplates { L3?: SampledValueTemplate } -export interface SampledValueTemplate extends SampledValue { +export type SampledValueTemplate = SampledValue & { fluctuationPercent?: number minimumValue?: number } diff --git a/src/types/index.ts b/src/types/index.ts index 5600888d..0884adb3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -143,15 +143,13 @@ export { } 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' @@ -176,6 +174,7 @@ export { OCPP20IncomingRequestCommand, type OCPP20NotifyReportRequest, OCPP20RequestCommand, + type OCPP20RequestStartTransactionRequest, type OCPP20ResetRequest, type OCPP20SetVariablesRequest, type OCPP20StatusNotificationRequest, @@ -187,10 +186,28 @@ export type { 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, @@ -204,7 +221,9 @@ export { type OCPP20SetVariableResultType, OCPP20VendorVariableName, PersistenceEnumType, + type ReportDataType, SetVariableStatusEnumType, + type VariableName, type VariableType, } from './ocpp/2.0/Variables.js' export { ChargePointErrorCode } from './ocpp/ChargePointErrorCode.js' diff --git a/src/types/ocpp/2.0/Common.ts b/src/types/ocpp/2.0/Common.ts index c5f370a8..0c54de3c 100644 --- a/src/types/ocpp/2.0/Common.ts +++ b/src/types/ocpp/2.0/Common.ts @@ -1,6 +1,5 @@ import type { JsonObject } from '../../JsonType.js' import type { GenericStatus } from '../Common.js' -import type { VariableType } from './Variables.js' export enum BootReasonEnumType { ApplicationReset = 'ApplicationReset', @@ -163,37 +162,41 @@ export enum OCPP20ComponentName { 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 { @@ -288,18 +291,18 @@ export interface ChargingStationType extends JsonObject { 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 @@ -308,36 +311,8 @@ export interface OCSPRequestDataType extends JsonObject { 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 -} diff --git a/src/types/ocpp/2.0/MeterValues.ts b/src/types/ocpp/2.0/MeterValues.ts index 503ca447..687379ec 100644 --- a/src/types/ocpp/2.0/MeterValues.ts +++ b/src/types/ocpp/2.0/MeterValues.ts @@ -1,6 +1,6 @@ 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', @@ -98,5 +98,5 @@ export interface OCPP20SignedMeterValue extends JsonObject { export interface OCPP20UnitOfMeasure extends JsonObject { customData?: CustomDataType multiplier?: number // Default: 0 - unit?: string // Default: "Wh" + unit?: OCPP20UnitEnumType } diff --git a/src/types/ocpp/2.0/Requests.ts b/src/types/ocpp/2.0/Requests.ts index 463bdb22..6971261f 100644 --- a/src/types/ocpp/2.0/Requests.ts +++ b/src/types/ocpp/2.0/Requests.ts @@ -3,13 +3,21 @@ import type { JsonObject } from '../../JsonType.js' 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', @@ -30,17 +38,20 @@ export enum OCPP20RequestCommand { 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[] } @@ -49,9 +60,11 @@ export type OCPP20HeartbeatRequest = EmptyObject export interface OCPP20InstallCertificateRequest extends JsonObject { certificate: string certificateType: InstallCertificateUseEnumType + customData?: CustomDataType } export interface OCPP20NotifyReportRequest extends JsonObject { + customData?: CustomDataType generatedAt: Date reportData?: ReportDataType[] requestId: number @@ -59,18 +72,30 @@ export interface OCPP20NotifyReportRequest extends JsonObject { 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 } diff --git a/src/types/ocpp/2.0/Responses.ts b/src/types/ocpp/2.0/Responses.ts index 648a8335..095d4b7d 100644 --- a/src/types/ocpp/2.0/Responses.ts +++ b/src/types/ocpp/2.0/Responses.ts @@ -2,52 +2,69 @@ import type { EmptyObject } from '../../EmptyObject.js' 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[] } diff --git a/src/types/ocpp/2.0/Transaction.ts b/src/types/ocpp/2.0/Transaction.ts new file mode 100644 index 00000000..e80b0acb --- /dev/null +++ b/src/types/ocpp/2.0/Transaction.ts @@ -0,0 +1,276 @@ +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[] +} diff --git a/src/types/ocpp/2.0/Variables.ts b/src/types/ocpp/2.0/Variables.ts index 0d94001c..dc4f4c64 100644 --- a/src/types/ocpp/2.0/Variables.ts +++ b/src/types/ocpp/2.0/Variables.ts @@ -1,5 +1,6 @@ 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', @@ -97,6 +98,7 @@ export interface OCPP20ComponentVariableType extends JsonObject { export interface OCPP20GetVariableDataType extends JsonObject { attributeType?: AttributeEnumType component: ComponentType + customData?: CustomDataType variable: VariableType } @@ -106,6 +108,7 @@ export interface OCPP20GetVariableResultType extends JsonObject { attributeType?: AttributeEnumType attributeValue?: string component: ComponentType + customData?: CustomDataType variable: VariableType } @@ -113,6 +116,7 @@ export interface OCPP20SetVariableDataType extends JsonObject { attributeType?: AttributeEnumType attributeValue: string component: ComponentType + customData?: CustomDataType variable: VariableType } @@ -121,17 +125,37 @@ export interface OCPP20SetVariableResultType extends JsonObject { 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 +} diff --git a/src/types/ocpp/ChargingProfile.ts b/src/types/ocpp/ChargingProfile.ts index 9009b76f..29cf7727 100644 --- a/src/types/ocpp/ChargingProfile.ts +++ b/src/types/ocpp/ChargingProfile.ts @@ -6,31 +6,47 @@ import { 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 diff --git a/src/types/ocpp/ConnectorEnumType.ts b/src/types/ocpp/ConnectorEnumType.ts index cc36ef56..1f2fdec4 100644 --- a/src/types/ocpp/ConnectorEnumType.ts +++ b/src/types/ocpp/ConnectorEnumType.ts @@ -1,4 +1,4 @@ -import { OCPP20ConnectorEnumType } from './2.0/Common.js' +import { OCPP20ConnectorEnumType } from './2.0/Transaction.js' export const ConnectorEnumType = { ...OCPP20ConnectorEnumType, diff --git a/src/types/ocpp/ConnectorStatusEnum.ts b/src/types/ocpp/ConnectorStatusEnum.ts index ee318291..7b2a091c 100644 --- a/src/types/ocpp/ConnectorStatusEnum.ts +++ b/src/types/ocpp/ConnectorStatusEnum.ts @@ -1,5 +1,5 @@ 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, diff --git a/src/types/ocpp/MeterValues.ts b/src/types/ocpp/MeterValues.ts index ff377bf7..988b7327 100644 --- a/src/types/ocpp/MeterValues.ts +++ b/src/types/ocpp/MeterValues.ts @@ -7,20 +7,24 @@ import { 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, @@ -50,4 +54,4 @@ export const MeterValuePhase = { // eslint-disable-next-line @typescript-eslint/no-redeclare export type MeterValuePhase = OCPP16MeterValuePhase | OCPP20PhaseEnumType -export type SampledValue = OCPP16SampledValue +export type SampledValue = OCPP16SampledValue | OCPP20SampledValue diff --git a/tests/ChargingStationFactory.test.ts b/tests/ChargingStationFactory.test.ts new file mode 100644 index 00000000..e808ddd6 --- /dev/null +++ b/tests/ChargingStationFactory.test.ts @@ -0,0 +1,419 @@ +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') + }) + }) + }) +}) diff --git a/tests/ChargingStationFactory.ts b/tests/ChargingStationFactory.ts index 9ab785cb..b42839cb 100644 --- a/tests/ChargingStationFactory.ts +++ b/tests/ChargingStationFactory.ts @@ -1,28 +1,47 @@ 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 @@ -33,8 +52,8 @@ const CHARGING_STATION_BASE_NAME = 'CS-TEST' /** * 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 @@ -43,17 +62,49 @@ export function createChargingStation (options: ChargingStationOptions = {}): Ch 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, @@ -64,6 +115,44 @@ export function createChargingStation (options: ChargingStationOptions = {}): Ch 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 */ @@ -75,27 +164,27 @@ export function createChargingStation (options: ChargingStationOptions = {}): Ch /* 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 @@ -106,33 +195,88 @@ export function createChargingStationTemplate ( } /** - * 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 + ) } diff --git a/tests/charging-station/Helpers.test.ts b/tests/charging-station/Helpers.test.ts index 43f8b529..eac06f09 100644 --- a/tests/charging-station/Helpers.test.ts +++ b/tests/charging-station/Helpers.test.ts @@ -29,7 +29,6 @@ import { createChargingStation, createChargingStationTemplate } from '../Chargin 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`) @@ -41,87 +40,273 @@ await describe('Helpers test suite', async () => { ) }) - 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` @@ -129,18 +314,27 @@ await describe('Helpers test suite', async () => { ) }) - 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') diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ClearCache.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ClearCache.test.ts index 0dfc4e76..1c100c48 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ClearCache.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ClearCache.test.ts @@ -6,25 +6,25 @@ 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 diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetBaseReport.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetBaseReport.test.ts index f69f8ce4..5340093b 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetBaseReport.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetBaseReport.test.ts @@ -17,6 +17,7 @@ import { OCPP20DeviceInfoVariableName, type OCPP20GetBaseReportRequest, type OCPP20SetVariableResultType, + OCPPVersion, ReportBaseEnumType, type ReportDataType, } from '../../../../src/types/index.js' @@ -27,18 +28,20 @@ import { } 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, @@ -46,6 +49,7 @@ await describe('B08 - Get Base Report', async () => { chargePointVendor: TEST_CHARGE_POINT_VENDOR, firmwareVersion: TEST_FIRMWARE_VERSION, ocppStrictCompliance: false, + ocppVersion: OCPPVersion.VERSION_201, }, websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL, }) @@ -152,13 +156,16 @@ await describe('B08 - Get Base Report', async () => { // 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, }, }) @@ -321,14 +328,16 @@ await describe('B08 - Get Base Report', async () => { // 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, }, }) @@ -344,7 +353,7 @@ await describe('B08 - Get Base Report', async () => { 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) } }) diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetVariables.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetVariables.test.ts index aa91669c..fa35a98d 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetVariables.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetVariables.test.ts @@ -12,11 +12,15 @@ import { 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, @@ -26,11 +30,14 @@ import { } 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, }) diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStartTransaction.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStartTransaction.test.ts new file mode 100644 index 00000000..e95e1a86 --- /dev/null +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStartTransaction.test.ts @@ -0,0 +1,195 @@ +/* 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) + }) +}) diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-Reset.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-Reset.test.ts index b195c087..39b8d6ee 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-Reset.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-Reset.test.ts @@ -10,20 +10,24 @@ import { OCPP20IncomingRequestService } from '../../../../src/charging-station/o 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, diff --git a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-SetVariables.test.ts b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-SetVariables.test.ts index c0a24ec1..61055614 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-SetVariables.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-SetVariables.test.ts @@ -14,12 +14,16 @@ import { 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, @@ -48,11 +52,14 @@ interface OCPP20GetVariablesRequest { /* 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, }) diff --git a/tests/charging-station/ocpp/2.0/OCPP20RequestService-BootNotification.test.ts b/tests/charging-station/ocpp/2.0/OCPP20RequestService-BootNotification.test.ts index fa6b179f..385a4193 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20RequestService-BootNotification.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20RequestService-BootNotification.test.ts @@ -12,6 +12,7 @@ import { 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' @@ -20,7 +21,7 @@ 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' @@ -29,7 +30,9 @@ await describe('B01 - Cold Boot Charging Station', async () => { 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, @@ -37,6 +40,7 @@ await describe('B01 - Cold Boot Charging Station', async () => { chargePointVendor: TEST_CHARGE_POINT_VENDOR, firmwareVersion: TEST_FIRMWARE_VERSION, ocppStrictCompliance: false, + ocppVersion: OCPPVersion.VERSION_201, }, websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL, }) diff --git a/tests/charging-station/ocpp/2.0/OCPP20RequestService-HeartBeat.test.ts b/tests/charging-station/ocpp/2.0/OCPP20RequestService-HeartBeat.test.ts index c6c2981c..d1a87c96 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20RequestService-HeartBeat.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20RequestService-HeartBeat.test.ts @@ -8,14 +8,18 @@ import { describe, it } from 'node:test' 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' @@ -24,7 +28,9 @@ await describe('G02 - Heartbeat', async () => { 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, @@ -32,6 +38,7 @@ await describe('G02 - Heartbeat', async () => { chargePointVendor: TEST_CHARGE_POINT_VENDOR, firmwareVersion: TEST_FIRMWARE_VERSION, ocppStrictCompliance: false, + ocppVersion: OCPPVersion.VERSION_201, }, websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL, }) @@ -117,6 +124,8 @@ await describe('G02 - Heartbeat', async () => { 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', @@ -124,6 +133,7 @@ await describe('G02 - Heartbeat', async () => { chargePointVendor: 'Alternative Vendor', firmwareVersion: '2.5.1', ocppStrictCompliance: true, + ocppVersion: OCPPVersion.VERSION_201, }, websocketPingInterval: 45, }) diff --git a/tests/charging-station/ocpp/2.0/OCPP20RequestService-NotifyReport.test.ts b/tests/charging-station/ocpp/2.0/OCPP20RequestService-NotifyReport.test.ts index 2d8b6305..4baf968f 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20RequestService-NotifyReport.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20RequestService-NotifyReport.test.ts @@ -16,6 +16,7 @@ import { type OCPP20NotifyReportRequest, OCPP20OptionalVariableName, OCPP20RequestCommand, + OCPPVersion, type ReportDataType, } from '../../../../src/types/index.js' import { Constants } from '../../../../src/utils/index.js' @@ -24,7 +25,7 @@ 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' @@ -33,7 +34,9 @@ await describe('B08 - NotifyReport', async () => { 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, @@ -41,6 +44,7 @@ await describe('B08 - NotifyReport', async () => { chargePointVendor: TEST_CHARGE_POINT_VENDOR, firmwareVersion: TEST_FIRMWARE_VERSION, ocppStrictCompliance: false, + ocppVersion: OCPPVersion.VERSION_201, }, websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL, }) diff --git a/tests/charging-station/ocpp/2.0/OCPP20RequestService-StatusNotification.test.ts b/tests/charging-station/ocpp/2.0/OCPP20RequestService-StatusNotification.test.ts index a29f2e67..35210bfb 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20RequestService-StatusNotification.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20RequestService-StatusNotification.test.ts @@ -12,23 +12,26 @@ import { 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, @@ -36,6 +39,7 @@ await describe('G01 - Status Notification', async () => { chargePointVendor: TEST_STATUS_CHARGE_POINT_VENDOR, firmwareVersion: TEST_FIRMWARE_VERSION, ocppStrictCompliance: false, + ocppVersion: OCPPVersion.VERSION_201, }, websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL, }) diff --git a/tests/charging-station/ocpp/2.0/OCPP20TestConstants.ts b/tests/charging-station/ocpp/2.0/OCPP20TestConstants.ts index 5ef7c187..92c1e577 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20TestConstants.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20TestConstants.ts @@ -3,7 +3,7 @@ */ // 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' @@ -14,7 +14,7 @@ export const TEST_CONNECTOR_VALID_INSTANCE = '1' 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' diff --git a/tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts b/tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts index 91504135..4fad8188 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts @@ -21,16 +21,14 @@ import { 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, @@ -61,9 +59,14 @@ function buildWsExampleUrl (targetLength: number, fillerChar = 'a'): string { 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, }) @@ -1380,7 +1383,12 @@ await describe('OCPP20VariableManager test suite', async () => { 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, })