]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
feat(ocpp2): add RequestStartTransaction command (#1583)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Mon, 3 Nov 2025 16:44:04 +0000 (17:44 +0100)
committerGitHub <noreply@github.com>
Mon, 3 Nov 2025 16:44:04 +0000 (17:44 +0100)
* feat(ocpp2): add RequestStartTransaction command

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* chore: refine opencode configuration

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* chore: refine opencode configuration

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* refactor: cleanups OCPP2 type definition

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* fix: ocpp2 type definition

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* 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 <jerome.benoit@sap.com>
* refactor: cleanup OCPP stack error messages

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* refactor: use enums in variables registry

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* 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 <jerome.benoit@sap.com>
* refactor: code formatting

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* 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 <jerome.benoit@sap.com>
* refactor: handle remoteStartId

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* docs: update README.md

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
---------

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
40 files changed:
.opencode/agent/review.md [new file with mode: 0644]
.opencode/command/format-simulator.md [new file with mode: 0644]
.opencode/command/test-simulator.md [moved from .opencode/command/tests-simulator.md with 57% similarity]
.opencode/command/tests-simulator-file.md [deleted file]
.serena/memories/code_style_conventions.md
README.md
eslint.config.js
src/charging-station/Helpers.ts
src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts
src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts
src/charging-station/ocpp/2.0/OCPP20VariableRegistry.ts
src/charging-station/ocpp/OCPPServiceUtils.ts
src/types/ConnectorStatus.ts
src/types/MeasurandPerPhaseSampledValueTemplates.ts
src/types/index.ts
src/types/ocpp/2.0/Common.ts
src/types/ocpp/2.0/MeterValues.ts
src/types/ocpp/2.0/Requests.ts
src/types/ocpp/2.0/Responses.ts
src/types/ocpp/2.0/Transaction.ts [new file with mode: 0644]
src/types/ocpp/2.0/Variables.ts
src/types/ocpp/ChargingProfile.ts
src/types/ocpp/ConnectorEnumType.ts
src/types/ocpp/ConnectorStatusEnum.ts
src/types/ocpp/MeterValues.ts
tests/ChargingStationFactory.test.ts [new file with mode: 0644]
tests/ChargingStationFactory.ts
tests/charging-station/Helpers.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-ClearCache.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetBaseReport.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-GetVariables.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-RequestStartTransaction.test.ts [new file with mode: 0644]
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-Reset.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-SetVariables.test.ts
tests/charging-station/ocpp/2.0/OCPP20RequestService-BootNotification.test.ts
tests/charging-station/ocpp/2.0/OCPP20RequestService-HeartBeat.test.ts
tests/charging-station/ocpp/2.0/OCPP20RequestService-NotifyReport.test.ts
tests/charging-station/ocpp/2.0/OCPP20RequestService-StatusNotification.test.ts
tests/charging-station/ocpp/2.0/OCPP20TestConstants.ts
tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts

diff --git a/.opencode/agent/review.md b/.opencode/agent/review.md
new file mode 100644 (file)
index 0000000..96f8b88
--- /dev/null
@@ -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 (file)
index 0000000..19f0eb3
--- /dev/null
@@ -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.
similarity index 57%
rename from .opencode/command/tests-simulator.md
rename to .opencode/command/test-simulator.md
index 0621de91f91b867799ee2767afe11a49aa41b8dc..60a15fded5527364da088b6f50b99752d0179aa4 100644 (file)
@@ -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 (file)
index a016456..0000000
+++ /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.
index 9d5f74986ff1a7723258e424e8ea8d22aedfe464..55b2fb87e4ebfc0d189ec08a56f165ecbe91cbc2 100644 (file)
@@ -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
 
index b1e3ae8651eec52247575093617d16333f8aba26..a212f34842ae11fe5501b392bb449abddf75bb26 100644 (file)
--- 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
 
index 6af2482edb4d0dd46f51b5ecfc97a3a7ef84827e..72c72c406c99cf881207eefefcf6183809e0a8c6 100644 (file)
@@ -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',
             ],
           },
         },
index 01eee0fcc802c5ea33660e23f0e3f035dc1ff7aa..c1edc53043c29ce01a43069be9475e6a7f46bbb6 100644 (file)
@@ -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<ChargingSchedulePeriod>(
+            // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
             chargingSchedule.chargingSchedulePeriod,
             chargingSchedulePeriodCompareFn
           )
         ) {
           logger.warn(
+            // eslint-disable-next-line @typescript-eslint/no-base-to-string
             `${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId.toString()} schedule periods are not sorted by start period`
           )
+          // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
           chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn)
         }
         // Check if the first schedule period startPeriod property is equal to 0
+        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
         if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
           logger.error(
+            // eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
             `${chargingStation.logPrefix()} ${moduleName}.getChargingProfilesLimit: Charging profile id ${chargingProfile.chargingProfileId.toString()} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod.toString()} is not equal to 0`
           )
           continue
         }
         // Handle only one schedule period
+        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
         if (chargingSchedule.chargingSchedulePeriod.length === 1) {
           const chargingProfilesLimit: ChargingProfilesLimit = {
             chargingProfile,
+            // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
             limit: chargingSchedule.chargingSchedulePeriod[0].limit,
           }
           logger.debug(debugLogMsg, chargingProfilesLimit)
@@ -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
index 0d55122b43bd199459335ff6d2d3208b24a744f1..621dc14fa5d14ff981a8e1858128c7c6db1cf3eb 100644 (file)
@@ -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),
index a1269b1d7a2e569ebd20b15eb4a510e903ff504e..605243657bfd25c3ae7a8a9a7b1deeb080161a81 100644 (file)
@@ -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<OCPP20RequestStartTransactionResponse> {
+    const { chargingProfile, evseId, groupIdToken, idToken, remoteStartId } = commandPayload
+    logger.info(
+      `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Remote start transaction request received on EVSE ${evseId?.toString() ?? 'undefined'} with idToken ${idToken.idToken} and remoteStartId ${remoteStartId.toString()}`
+    )
+
+    // Validate that EVSE ID is provided
+    if (evseId == null) {
+      const errorMsg = 'EVSE ID is required for RequestStartTransaction'
+      logger.error(
+        `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: ${errorMsg}`
+      )
+      throw new OCPPError(
+        ErrorType.PROPERTY_CONSTRAINT_VIOLATION,
+        errorMsg,
+        OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION,
+        commandPayload
+      )
+    }
+
+    // Get the first connector for this EVSE
+    const evse = chargingStation.evses.get(evseId)
+    if (evse == null) {
+      const errorMsg = `EVSE ${String(evseId)} not found on charging station`
+      logger.error(
+        `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: ${errorMsg}`
+      )
+      throw new OCPPError(
+        ErrorType.PROPERTY_CONSTRAINT_VIOLATION,
+        errorMsg,
+        OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION,
+        commandPayload
+      )
+    }
+    const connectorId: number | undefined = evse.connectors.keys().next().value
+    const connectorStatus =
+      connectorId != null ? chargingStation.getConnectorStatus(connectorId) : null
+
+    if (connectorStatus == null || connectorId == null) {
+      const errorMsg = `Connector ${connectorId?.toString() ?? 'undefined'} status is undefined`
+      logger.error(
+        `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: ${errorMsg}`
+      )
+      throw new OCPPError(
+        ErrorType.INTERNAL_ERROR,
+        errorMsg,
+        OCPP20IncomingRequestCommand.REQUEST_START_TRANSACTION,
+        commandPayload
+      )
+    }
+
+    // Check if connector is available for a new transaction
+    if (connectorStatus.transactionStarted === true) {
+      logger.warn(
+        `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Connector ${connectorId.toString()} already has an active transaction`
+      )
+      return {
+        status: RequestStartStopStatusEnumType.Rejected,
+        transactionId: generateUUID(),
+      }
+    }
+
+    // Authorize idToken
+    let isAuthorized = false
+    try {
+      isAuthorized = await this.isIdTokenAuthorized(chargingStation, idToken)
+    } catch (error) {
+      logger.error(
+        `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Authorization error for ${idToken.idToken}:`,
+        error
+      )
+      return {
+        status: RequestStartStopStatusEnumType.Rejected,
+        transactionId: generateUUID(),
+      }
+    }
+
+    if (!isAuthorized) {
+      logger.warn(
+        `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: IdToken ${idToken.idToken} is not authorized`
+      )
+      return {
+        status: RequestStartStopStatusEnumType.Rejected,
+        transactionId: generateUUID(),
+      }
+    }
+
+    // Authorize groupIdToken if provided
+    if (groupIdToken != null) {
+      let isGroupAuthorized = false
+      try {
+        isGroupAuthorized = await this.isIdTokenAuthorized(chargingStation, groupIdToken)
+      } catch (error) {
+        logger.error(
+          `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Group authorization error for ${groupIdToken.idToken}:`,
+          error
+        )
+        return {
+          status: RequestStartStopStatusEnumType.Rejected,
+          transactionId: generateUUID(),
+        }
+      }
+
+      if (!isGroupAuthorized) {
+        logger.warn(
+          `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: GroupIdToken ${groupIdToken.idToken} is not authorized`
+        )
+        return {
+          status: RequestStartStopStatusEnumType.Rejected,
+          transactionId: generateUUID(),
+        }
+      }
+    }
+
+    // Validate charging profile if provided
+    if (chargingProfile != null) {
+      let isValidProfile = false
+      try {
+        isValidProfile = this.validateChargingProfile(chargingStation, chargingProfile, evseId)
+      } catch (error) {
+        logger.error(
+          `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Charging profile validation error:`,
+          error
+        )
+        return {
+          status: RequestStartStopStatusEnumType.Rejected,
+          transactionId: generateUUID(),
+        }
+      }
+
+      if (!isValidProfile) {
+        logger.warn(
+          `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Invalid charging profile`
+        )
+        return {
+          status: RequestStartStopStatusEnumType.Rejected,
+          transactionId: generateUUID(),
+        }
+      }
+    }
+
+    const transactionId = generateUUID()
+
+    // Backup current connector state in case we need to rollback
+    const connectorBackup = {
+      remoteStartId: connectorStatus.remoteStartId,
+      status: connectorStatus.status,
+      transactionEnergyActiveImportRegisterValue:
+        connectorStatus.transactionEnergyActiveImportRegisterValue,
+      transactionId: connectorStatus.transactionId,
+      transactionIdTag: connectorStatus.transactionIdTag,
+      transactionStart: connectorStatus.transactionStart,
+      transactionStarted: connectorStatus.transactionStarted,
+    }
+
+    try {
+      // Set connector transaction state
+      connectorStatus.transactionStarted = true
+      connectorStatus.transactionId = transactionId
+      connectorStatus.transactionIdTag = idToken.idToken
+      connectorStatus.transactionStart = new Date()
+      connectorStatus.transactionEnergyActiveImportRegisterValue = 0
+      connectorStatus.remoteStartId = remoteStartId
+
+      // Update connector status to Occupied
+      await sendAndSetConnectorStatus(
+        chargingStation,
+        connectorId,
+        ConnectorStatusEnum.Occupied,
+        evseId
+      )
+
+      // Store charging profile if provided
+      if (chargingProfile != null) {
+        connectorStatus.chargingProfiles ??= []
+        connectorStatus.chargingProfiles.push(chargingProfile)
+        // TODO: Implement charging profile storage
+        logger.debug(
+          `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Charging profile stored for transaction ${transactionId} (TODO: implement profile storage)`
+        )
+      }
+
+      logger.info(
+        `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Remote start transaction accepted on EVSE ${evseId.toString()}, connector ${connectorId.toString()} with transaction ID ${transactionId} for idToken ${idToken.idToken}`
+      )
+
+      return {
+        status: RequestStartStopStatusEnumType.Accepted,
+        transactionId,
+      }
+    } catch (error) {
+      // Rollback connector state on error
+      connectorStatus.transactionStarted = connectorBackup.transactionStarted
+      connectorStatus.transactionId = connectorBackup.transactionId
+      connectorStatus.transactionIdTag = connectorBackup.transactionIdTag
+      connectorStatus.transactionStart = connectorBackup.transactionStart
+      connectorStatus.transactionEnergyActiveImportRegisterValue =
+        connectorBackup.transactionEnergyActiveImportRegisterValue
+      connectorStatus.status = connectorBackup.status
+      connectorStatus.remoteStartId = connectorBackup.remoteStartId
+
+      logger.error(
+        `${chargingStation.logPrefix()} ${moduleName}.handleRequestRequestStartTransaction: Error starting transaction:`,
+        error
+      )
+      return {
+        status: RequestStartStopStatusEnumType.Rejected,
+        transactionId: generateUUID(),
+      }
+    }
+  }
+
   private handleRequestReset (
     chargingStation: ChargingStation,
     commandPayload: OCPP20ResetRequest
@@ -1053,6 +1280,27 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
     }
   }
 
+  // Helper methods for RequestStartTransaction
+  private async isIdTokenAuthorized (
+    chargingStation: ChargingStation,
+    idToken: OCPP20IdTokenType
+  ): Promise<boolean> {
+    // TODO: Implement proper authorization logic
+    // This should check:
+    // 1. Local authorization list if enabled
+    // 2. Remote authorization via AuthorizeRequest if needed
+    // 3. Cache for known tokens
+    // 4. Return false if authorization fails
+
+    logger.debug(
+      `${chargingStation.logPrefix()} ${moduleName}.isIdTokenAuthorized: Validating idToken ${idToken.idToken} of type ${idToken.type}`
+    )
+
+    // For now, return true to allow development/testing
+    // TODO: Implement actual async authorization logic
+    return await Promise.resolve(true)
+  }
+
   private scheduleEvseReset (
     chargingStation: ChargingStation,
     evseId: number,
@@ -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,
index 934a4267ab00f7c7637b362bd3b52c469ebe0cd0..c536e6211d7536b0d12426d56ca9ef80f805cdd4 100644 (file)
@@ -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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     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<string, VariableMetadata> = {
     persistence: PersistenceEnumType.Persistent,
     positive: true,
     supportedAttributes: [AttributeEnumType.Actual],
-    unit: 's',
+    unit: OCPP20UnitEnumType.SECONDS,
     variable: OCPP20RequiredVariableName.EVConnectionTimeOut as string,
   },
   [buildRegistryKey(
index 36d66fcb4e3adee5653b2b368c254071915dac62..83288e9b2d918f02c1a635cae5fe811fa8790b02 100644 (file)
@@ -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 = (
index d77a874422c540b8a11b4bcf051049f54d75d301..b3ebfe150dfbdbf0a86c7259ca0863c66a09a939 100644 (file)
@@ -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
index f35f4f64fe9079df592f1fb3ccdc9265d52c0f06..7c262192315de9e1f4e7fbe4b7050cc27f1141cd 100644 (file)
@@ -6,7 +6,7 @@ export interface MeasurandPerPhaseSampledValueTemplates {
   L3?: SampledValueTemplate
 }
 
-export interface SampledValueTemplate extends SampledValue {
+export type SampledValueTemplate = SampledValue & {
   fluctuationPercent?: number
   minimumValue?: number
 }
index 5600888d7b5e54d1661864f68cf7fc77bdce22f5..0884adb367532131ae09fe532d44ddf08989f8c5 100644 (file)
@@ -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'
index c5f370a84b4df04e9dcebe9fde564abed69e8efb..0c54de3c7c165fa41ae68b2c094f20889a7a4764 100644 (file)
@@ -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
-}
index 503ca4473e96ff57048ba9fcfe2e3bbca12eff82..687379ec3b2486bc1b0bbb2628d187796637ce3b 100644 (file)
@@ -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
 }
index 463bdb224d0857ac3e660ecfe5e976ecec0dc1e0..6971261ff60ce053698d0a0d53754fb1c88d97af 100644 (file)
@@ -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
 }
index 648a83359292d09ac2e67159627c65a2a94a22b3..095d4b7d74188a762330f7b5da431b8d8ea3b76e 100644 (file)
@@ -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 (file)
index 0000000..e80b0ac
--- /dev/null
@@ -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[]
+}
index 0d94001c936cad9ad927d1e0fce0c4d33362dbe3..dc4f4c64bc107ffc9d78b3123db0a3b19cde3900 100644 (file)
@@ -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
+}
index 9009b76fce9da759b766a05ac5dbc37ab648b143..29cf772726a370b766eefce2c4af51f9369ebd3a 100644 (file)
@@ -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
index cc36ef56bea156507a21fc6840d9bad9b78a4c09..1f2fdec40f23024c8582dbd68394b1b3a12d1805 100644 (file)
@@ -1,4 +1,4 @@
-import { OCPP20ConnectorEnumType } from './2.0/Common.js'
+import { OCPP20ConnectorEnumType } from './2.0/Transaction.js'
 
 export const ConnectorEnumType = {
   ...OCPP20ConnectorEnumType,
index ee31829194f7e9d52579e3fe40f9761d0d622b0b..7b2a091c403b9c3f77581f6b242f65a409341b58 100644 (file)
@@ -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,
index ff377bf728754da0e71e959cd806d6cd8641cece..988b73271167c2c1078827554663c3911219a8b0 100644 (file)
@@ -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 (file)
index 0000000..e808ddd
--- /dev/null
@@ -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')
+      })
+    })
+  })
+})
index 9ab785cbe57c2ca26352e98a3f817e4604eadea7..b42839cb321557ab44ef0a2a36071d8578fd6d8f 100644 (file)
@@ -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<ChargingStationInfo>
@@ -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
+  )
 }
index 43f8b529281c9fc478e88e3e05efefc1020cbcde..eac06f09d1b81371985668590ed163075fc2cf8c 100644 (file)
@@ -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')
index 0dfc4e768dd98de3b6606dea242aca6135748f28..1c100c48b62591aaa7a435574844b100bdd81e09 100644 (file)
@@ -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
index f69f8ce44d9925fc80dfa1568256dbe6991999a9..5340093b752990abd984a7d6a3b21a63816428e6 100644 (file)
@@ -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)
     }
   })
index aa91669c8c45c4e49cca703554b7f70dc726c599..fa35a98d569150345c84edec4fcbd699044fe171 100644 (file)
@@ -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 (file)
index 0000000..e95e1a8
--- /dev/null
@@ -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)
+  })
+})
index b195c08762b453a1944908c2d7d535a4ade6340b..39b8d6ee0b6696356ad33538ee28489cc94072b3 100644 (file)
@@ -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,
index c0a24ec1255e0e6a982df23ddb71f2afbb953837..610556145108ffa0f2b6be573432ec198becde32 100644 (file)
@@ -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,
   })
index fa6b179faccae84407973bab8fae532ac4fb915f..385a41935adcc18b732357719ac4fbf1efe75e38 100644 (file)
@@ -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,
   })
index c6c2981c3032ff5b11cf790e4c0f20b039283970..d1a87c96cc57e9492dc34086dc1a02602aaff503 100644 (file)
@@ -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,
     })
index 2d8b6305f88a99d4bf010f01fac97327a6c34245..4baf968fb673f2ef20833364e1e85e534508fc84 100644 (file)
@@ -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,
   })
index a29f2e670661c2e88d79913ef03fa2ae0714ec98..35210bfb752a134d40fdee6c74bd51af15abbd1b 100644 (file)
@@ -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,
   })
index 5ef7c187fd645dec9816d110f2ab8bdda1aee416..92c1e5774eb5fa4d06e86b7b20880d01d8a50c33 100644 (file)
@@ -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'
index 91504135b6dc632bc8fae53b19dc4e42558afb4e..4fad8188c2ccbcd8c8eca811042c55e60f9ac116 100644 (file)
@@ -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,
     })