BootReasonEnumType,
type ChargingStationInfo,
type ConfigurationKeyType,
+ CurrentType,
ErrorType,
+ type MeasurandPerPhaseSampledValueTemplates,
+ type MeasurandValues,
type MeterValueContext,
type MeterValuePhase,
MeterValueUnit,
RequestCommand,
type SampledValueTemplate,
} from '../../../types/index.js'
-import { roundTo } from '../../../utils/index.js'
+import { ACElectricUtils, DCElectricUtils, roundTo } from '../../../utils/index.js'
import {
addLineToLineVoltageToMeterValue,
addMainVoltageToMeterValue,
buildVoltageMeasurandValue,
resolveSampledValueFields,
updateConnectorEnergyValues,
+ validateCurrentMeasurandPhaseValue,
+ validateCurrentMeasurandValue,
validateEnergyMeasurandValue,
+ validatePowerMeasurandValue,
validateSocMeasurandValue,
} from '../OCPPServiceUtils.js'
)
}
}
- // Energy.Active.Import.Register measurand
+ // Power.Active.Import measurand
+ const powerMeasurand = buildPowerMeasurandValue(
+ chargingStation,
+ connectorId,
+ evseId,
+ measurandsKey
+ )
+ if (powerMeasurand?.values.allPhases != null) {
+ const unitDivider = powerMeasurand.template.unit === MeterValueUnit.KILO_WATT ? 1000 : 1
+ const connectorMaximumAvailablePower =
+ chargingStation.getConnectorMaximumAvailablePower(connectorId)
+ const connectorMaximumPower = Math.round(connectorMaximumAvailablePower)
+ const connectorMinimumPower = Math.round(powerMeasurand.template.minimumValue ?? 0)
+
+ meterValue.sampledValue.push(
+ buildVersionedSampledValue(powerMeasurand.template, powerMeasurand.values.allPhases, context)
+ )
+ const sampledValuesIndex = meterValue.sampledValue.length - 1
+ validatePowerMeasurandValue(
+ chargingStation,
+ connectorId,
+ connectorStatus,
+ meterValue.sampledValue[sampledValuesIndex],
+ connectorMaximumPower / unitDivider,
+ connectorMinimumPower / unitDivider,
+ debug
+ )
+ if (chargingStation.getNumberOfPhases() === 3) {
+ const connectorMaximumPowerPerPhase = Math.round(
+ connectorMaximumAvailablePower / chargingStation.getNumberOfPhases()
+ )
+ const connectorMinimumPowerPerPhase = Math.round(
+ connectorMinimumPower / chargingStation.getNumberOfPhases()
+ )
+ for (let phase = 1; phase <= chargingStation.getNumberOfPhases(); phase++) {
+ const phaseTemplate =
+ powerMeasurand.perPhaseTemplates[
+ `L${phase.toString()}` as keyof MeasurandPerPhaseSampledValueTemplates
+ ]
+ if (phaseTemplate != null) {
+ const phaseValue = `L${phase.toString()}-N` as MeterValuePhase
+ const phasePowerValue =
+ powerMeasurand.values[`L${phase.toString()}` as keyof MeasurandValues]
+ meterValue.sampledValue.push(
+ buildVersionedSampledValue(phaseTemplate, phasePowerValue, context, phaseValue)
+ )
+ const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1
+ validatePowerMeasurandValue(
+ chargingStation,
+ connectorId,
+ connectorStatus,
+ meterValue.sampledValue[sampledValuesPerPhaseIndex],
+ connectorMaximumPowerPerPhase / unitDivider,
+ connectorMinimumPowerPerPhase / unitDivider,
+ debug
+ )
+ }
+ }
+ }
+ }
+ // Current.Import measurand
+ const currentMeasurand = buildCurrentMeasurandValue(
+ chargingStation,
+ connectorId,
+ evseId,
+ measurandsKey
+ )
+ if (currentMeasurand?.values.allPhases != null) {
+ const connectorMaximumAvailablePower =
+ chargingStation.getConnectorMaximumAvailablePower(connectorId)
+ const connectorMaximumAmperage =
+ chargingStation.stationInfo?.currentOutType === CurrentType.AC
+ ? ACElectricUtils.amperagePerPhaseFromPower(
+ chargingStation.getNumberOfPhases(),
+ connectorMaximumAvailablePower,
+ chargingStation.getVoltageOut()
+ )
+ : DCElectricUtils.amperage(connectorMaximumAvailablePower, chargingStation.getVoltageOut())
+ const connectorMinimumAmperage = currentMeasurand.template.minimumValue ?? 0
+
+ meterValue.sampledValue.push(
+ buildVersionedSampledValue(
+ currentMeasurand.template,
+ currentMeasurand.values.allPhases,
+ context
+ )
+ )
+ const sampledValuesIndex = meterValue.sampledValue.length - 1
+ validateCurrentMeasurandValue(
+ chargingStation,
+ connectorId,
+ connectorStatus,
+ meterValue.sampledValue[sampledValuesIndex],
+ connectorMaximumAmperage,
+ connectorMinimumAmperage,
+ debug
+ )
+ for (
+ let phase = 1;
+ chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
+ phase++
+ ) {
+ const phaseValue = `L${phase.toString()}` as MeterValuePhase
+ meterValue.sampledValue.push(
+ buildVersionedSampledValue(
+ currentMeasurand.perPhaseTemplates[
+ phaseValue as keyof MeasurandPerPhaseSampledValueTemplates
+ ] ?? currentMeasurand.template,
+ currentMeasurand.values[phaseValue as keyof MeasurandPerPhaseSampledValueTemplates],
+ context,
+ phaseValue
+ )
+ )
+ const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1
+ validateCurrentMeasurandPhaseValue(
+ chargingStation,
+ connectorId,
+ connectorStatus,
+ meterValue.sampledValue[sampledValuesPerPhaseIndex],
+ connectorMaximumAmperage,
+ connectorMinimumAmperage,
+ debug
+ )
+ }
+ }
+ // Energy.Active.Import.Register measurand (default)
const energyMeasurand = buildEnergyMeasurandValue(
chargingStation,
connectorId,
debug
)
}
- // Power.Active.Import measurand
- const powerMeasurand = buildPowerMeasurandValue(
- chargingStation,
- connectorId,
- evseId,
- measurandsKey
- )
- if (powerMeasurand?.values.allPhases != null) {
- const powerSampledValue = buildVersionedSampledValue(
- powerMeasurand.template,
- powerMeasurand.values.allPhases,
- context
- )
- meterValue.sampledValue.push(powerSampledValue)
- }
- // Current.Import measurand
- const currentMeasurand = buildCurrentMeasurandValue(
- chargingStation,
- connectorId,
- evseId,
- measurandsKey
- )
- if (currentMeasurand?.values.allPhases != null) {
- const currentSampledValue = buildVersionedSampledValue(
- currentMeasurand.template,
- currentMeasurand.values.allPhases,
- context
- )
- meterValue.sampledValue.push(currentSampledValue)
- }
return meterValue
}
})
})
+ await it('should restart transaction updated interval for OCPP 2.0', async t => {
+ await withMockTimers(t, ['setInterval'], () => {
+ // Arrange
+ const result = createMockChargingStation({
+ connectorsCount: 2,
+ ocppVersion: OCPPVersion.VERSION_20,
+ })
+ station = result.station
+ const connector1 = station.getConnectorStatus(1)
+ if (connector1 != null) {
+ connector1.transactionStarted = true
+ connector1.transactionId = 100
+ }
+ OCPP20ServiceUtils.startUpdatedMeterValues(station, 1, 5000)
+ const firstInterval = connector1?.transactionUpdatedMeterValuesSetInterval
+
+ // Act
+ OCPP20ServiceUtils.stopUpdatedMeterValues(station, 1)
+ OCPP20ServiceUtils.startUpdatedMeterValues(station, 1, 8000)
+ const secondInterval = connector1?.transactionUpdatedMeterValuesSetInterval
+
+ // Assert - interval should be different
+ assert.notStrictEqual(secondInterval, undefined)
+ assert.strictEqual(typeof secondInterval, 'object')
+ assert.notStrictEqual(firstInterval, secondInterval)
+ })
+ })
+
await it('should create transaction ended interval when startEndedMeterValues() is called for OCPP 2.0', async t => {
await withMockTimers(t, ['setInterval'], () => {
// Arrange
import {
AvailabilityType,
ConnectorStatusEnum,
+ CurrentType,
OCPPVersion,
RegistrationStatusEnumType,
} from '../../../src/types/index.js'
getNumberOfEvses (): number {
return evses.has(0) ? evses.size - 1 : evses.size
},
+ getNumberOfPhases (): number {
+ return stationInfoOverrides?.numberOfPhases ?? 3
+ },
getNumberOfRunningTransactions (): number {
return this.iterateConnectors(true).reduce(
(count, { connectorStatus }) =>
({ connectorStatus }) => connectorStatus.transactionId === transactionId
)?.connectorStatus.transactionIdTag
},
+ getVoltageOut (): number {
+ return stationInfoOverrides?.voltageOut ?? 230
+ },
getWebSocketPingInterval (): number {
return websocketPingInterval
},
autoStart,
baseName,
chargingStationId: `${baseName}-${index.toString().padStart(5, '0')}`,
+ currentOutType: CurrentType.AC,
hashId: TEST_CHARGING_STATION_HASH_ID,
maximumAmperage: 32,
maximumPower: 22000,
+ numberOfPhases: 3,
ocppVersion: stationInfoOverrides?.ocppVersion ?? ocppVersion,
remoteAuthorization: true,
templateIndex: index,
templateName: templateFile,
+ voltageOut: 230,
...stationInfoOverrides,
},
--- /dev/null
+/**
+ * @file Tests for OCPP16ServiceUtils meter value building functions
+ * @module OCPP 1.6 — §4.7 MeterValues (meter value building)
+ * @description Verifies pure static methods on OCPP16ServiceUtils for meter value building:
+ * buildTransactionBeginMeterValue, buildTransactionDataMeterValues, buildTransactionEndMeterValue.
+ */
+
+import assert from 'node:assert/strict'
+import { afterEach, describe, it } from 'node:test'
+
+import { OCPP16ServiceUtils } from '../../../../src/charging-station/ocpp/1.6/OCPP16ServiceUtils.js'
+import {
+ type OCPP16MeterValue,
+ OCPP16MeterValueContext,
+ OCPP16MeterValueMeasurand,
+ OCPP16MeterValueUnit,
+ OCPPVersion,
+} from '../../../../src/types/index.js'
+import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
+import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
+import { createMeterValuesTemplate } from './OCPP16TestUtils.js'
+
+await describe('OCPP16ServiceUtils — MeterValues', async () => {
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ // ─── buildTransactionBeginMeterValue ───────────────────────────────────
+
+ await describe('buildTransactionBeginMeterValue', async () => {
+ await it('should return a meter value with Transaction.Begin context when template exists', () => {
+ // Arrange
+ const { station } = createMockChargingStation({
+ ocppVersion: OCPPVersion.VERSION_16,
+ stationInfo: { ocppVersion: OCPPVersion.VERSION_16 },
+ })
+ const connectorStatus = station.getConnectorStatus(1)
+ if (connectorStatus != null) {
+ connectorStatus.MeterValues = createMeterValuesTemplate([
+ {
+ measurand: OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
+ unit: OCPP16MeterValueUnit.WATT_HOUR,
+ value: '0',
+ },
+ ])
+ }
+
+ // Act
+ const meterValue = OCPP16ServiceUtils.buildTransactionBeginMeterValue(station, 1, 1000)
+
+ // Assert
+ assert.notStrictEqual(meterValue, undefined)
+ assert.ok(meterValue.timestamp instanceof Date)
+ assert.strictEqual(Array.isArray(meterValue.sampledValue), true)
+ assert.strictEqual(meterValue.sampledValue.length, 1)
+ assert.strictEqual(
+ meterValue.sampledValue[0].context,
+ OCPP16MeterValueContext.TRANSACTION_BEGIN
+ )
+ })
+
+ await it('should apply Wh unit divider of 1 for meterStart', () => {
+ // Arrange
+ const { station } = createMockChargingStation({
+ ocppVersion: OCPPVersion.VERSION_16,
+ stationInfo: { ocppVersion: OCPPVersion.VERSION_16 },
+ })
+ const connectorStatus = station.getConnectorStatus(1)
+ if (connectorStatus != null) {
+ connectorStatus.MeterValues = createMeterValuesTemplate([
+ {
+ measurand: OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
+ unit: OCPP16MeterValueUnit.WATT_HOUR,
+ value: '0',
+ },
+ ])
+ }
+
+ // Act
+ const meterValue = OCPP16ServiceUtils.buildTransactionBeginMeterValue(station, 1, 5000)
+
+ // Assert — Wh divider is 1, so value = 5000 / 1 = 5000
+ assert.strictEqual(meterValue.sampledValue[0].value, '5000')
+ })
+
+ await it('should apply kWh unit divider of 1000 for meterStart', () => {
+ // Arrange
+ const { station } = createMockChargingStation({
+ ocppVersion: OCPPVersion.VERSION_16,
+ stationInfo: { ocppVersion: OCPPVersion.VERSION_16 },
+ })
+ const connectorStatus = station.getConnectorStatus(1)
+ if (connectorStatus != null) {
+ connectorStatus.MeterValues = createMeterValuesTemplate([
+ {
+ measurand: OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
+ unit: OCPP16MeterValueUnit.KILO_WATT_HOUR,
+ value: '0',
+ },
+ ])
+ }
+
+ // Act
+ const meterValue = OCPP16ServiceUtils.buildTransactionBeginMeterValue(station, 1, 5000)
+
+ // Assert — kWh divider is 1000, so value = 5000 / 1000 = 5
+ assert.strictEqual(meterValue.sampledValue[0].value, '5')
+ })
+
+ await it('should use meterStart 0 when undefined', () => {
+ // Arrange
+ const { station } = createMockChargingStation({
+ ocppVersion: OCPPVersion.VERSION_16,
+ stationInfo: { ocppVersion: OCPPVersion.VERSION_16 },
+ })
+ const connectorStatus = station.getConnectorStatus(1)
+ if (connectorStatus != null) {
+ connectorStatus.MeterValues = createMeterValuesTemplate([
+ {
+ measurand: OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
+ unit: OCPP16MeterValueUnit.WATT_HOUR,
+ value: '0',
+ },
+ ])
+ }
+
+ // Act
+ const meterValue = OCPP16ServiceUtils.buildTransactionBeginMeterValue(station, 1, undefined)
+
+ // Assert — undefined meterStart defaults to 0
+ assert.strictEqual(meterValue.sampledValue[0].value, '0')
+ })
+
+ await it('should throw when MeterValues template is empty (missing default measurand)', () => {
+ const { station } = createMockChargingStation({
+ ocppVersion: OCPPVersion.VERSION_16,
+ stationInfo: { ocppVersion: OCPPVersion.VERSION_16 },
+ })
+
+ assert.throws(
+ () => {
+ OCPP16ServiceUtils.buildTransactionBeginMeterValue(station, 1, 100)
+ },
+ { message: /Missing MeterValues for default measurand/ }
+ )
+ })
+ })
+
+ // ─── buildTransactionDataMeterValues ───────────────────────────────────
+
+ await describe('buildTransactionDataMeterValues', async () => {
+ await it('should return array containing both begin and end meter values', () => {
+ // Arrange
+ const beginMeterValue: OCPP16MeterValue = {
+ sampledValue: [{ context: OCPP16MeterValueContext.TRANSACTION_BEGIN, value: '0' }],
+ timestamp: new Date('2025-01-01T00:00:00Z'),
+ } as OCPP16MeterValue
+ const endMeterValue: OCPP16MeterValue = {
+ sampledValue: [{ context: OCPP16MeterValueContext.TRANSACTION_END, value: '100' }],
+ timestamp: new Date('2025-01-01T01:00:00Z'),
+ } as OCPP16MeterValue
+
+ // Act
+ const result = OCPP16ServiceUtils.buildTransactionDataMeterValues(
+ beginMeterValue,
+ endMeterValue
+ )
+
+ // Assert
+ assert.strictEqual(result.length, 2)
+ assert.strictEqual(result[0], beginMeterValue)
+ assert.strictEqual(result[1], endMeterValue)
+ })
+
+ await it('should return a new array instance', () => {
+ const beginMeterValue: OCPP16MeterValue = {
+ sampledValue: [],
+ timestamp: new Date(),
+ } as OCPP16MeterValue
+ const endMeterValue: OCPP16MeterValue = {
+ sampledValue: [],
+ timestamp: new Date(),
+ } as OCPP16MeterValue
+
+ const result1 = OCPP16ServiceUtils.buildTransactionDataMeterValues(
+ beginMeterValue,
+ endMeterValue
+ )
+ const result2 = OCPP16ServiceUtils.buildTransactionDataMeterValues(
+ beginMeterValue,
+ endMeterValue
+ )
+
+ // Different array instances
+ assert.notStrictEqual(result1, result2)
+ })
+ })
+
+ // ─── buildTransactionEndMeterValue ─────────────────────────────────────
+
+ await describe('buildTransactionEndMeterValue', async () => {
+ await it('should return a meter value with Transaction.End context', () => {
+ // Arrange
+ const { station } = createMockChargingStation({
+ ocppVersion: OCPPVersion.VERSION_16,
+ stationInfo: { ocppVersion: OCPPVersion.VERSION_16 },
+ })
+ const connectorStatus = station.getConnectorStatus(1)
+ if (connectorStatus != null) {
+ connectorStatus.MeterValues = createMeterValuesTemplate([
+ {
+ measurand: OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
+ unit: OCPP16MeterValueUnit.WATT_HOUR,
+ value: '0',
+ },
+ ])
+ }
+
+ // Act
+ const meterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(station, 1, 10000)
+
+ // Assert
+ assert.notStrictEqual(meterValue, undefined)
+ assert.ok(meterValue.timestamp instanceof Date)
+ assert.strictEqual(meterValue.sampledValue.length, 1)
+ assert.strictEqual(
+ meterValue.sampledValue[0].context,
+ OCPP16MeterValueContext.TRANSACTION_END
+ )
+ })
+
+ await it('should apply kWh unit divider for end meter value', () => {
+ // Arrange
+ const { station } = createMockChargingStation({
+ ocppVersion: OCPPVersion.VERSION_16,
+ stationInfo: { ocppVersion: OCPPVersion.VERSION_16 },
+ })
+ const connectorStatus = station.getConnectorStatus(1)
+ if (connectorStatus != null) {
+ connectorStatus.MeterValues = createMeterValuesTemplate([
+ {
+ measurand: OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
+ unit: OCPP16MeterValueUnit.KILO_WATT_HOUR,
+ value: '0',
+ },
+ ])
+ }
+
+ // Act
+ const meterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(station, 1, 3000)
+
+ // Assert — kWh divider: 3000 / 1000 = 3
+ assert.strictEqual(meterValue.sampledValue[0].value, '3')
+ })
+ })
+})
/**
* @file Tests for OCPP16ServiceUtils pure utility functions
- * @module OCPP 1.6 — §4.7 MeterValues (meter value building), §9.3 SetChargingProfile
- * (charging profile management), §3 ChargePoint status (connector status transitions),
- * §9.4 ClearChargingProfile (Errata 3.25 AND logic), authorization cache updates
- * @description Verifies pure static methods on OCPP16ServiceUtils: meter value building,
- * charging profile management, feature profile checking, command support checks,
- * and authorization cache update behavior.
+ * @module OCPP 1.6 — §9.3 SetChargingProfile (charging profile management), §3 ChargePoint status
+ * (connector status transitions), §9.4 ClearChargingProfile (Errata 3.25 AND logic),
+ * authorization cache updates
+ * @description Verifies pure static methods on OCPP16ServiceUtils: charging profile management,
+ * feature profile checking, command support checks, and authorization cache update behavior.
*/
import assert from 'node:assert/strict'
type OCPP16ClearChargingProfileRequest,
type OCPP16IdTagInfo,
OCPP16IncomingRequestCommand,
- type OCPP16MeterValue,
- OCPP16MeterValueContext,
- OCPP16MeterValueMeasurand,
- OCPP16MeterValueUnit,
OCPP16RequestCommand,
OCPP16StandardParametersKey,
type OCPP16StatusNotificationRequest,
import { TEST_CHARGING_STATION_BASE_NAME, TEST_ID_TAG } from '../../ChargingStationTestConstants.js'
import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
import { getTestAuthCache } from '../auth/helpers/MockFactories.js'
-import { createCommandsSupport, createMeterValuesTemplate } from './OCPP16TestUtils.js'
+import { createCommandsSupport } from './OCPP16TestUtils.js'
await describe('OCPP16ServiceUtils — pure functions', async () => {
afterEach(() => {
standardCleanup()
})
- // ─── buildTransactionBeginMeterValue ───────────────────────────────────
-
- await describe('buildTransactionBeginMeterValue', async () => {
- await it('should return a meter value with Transaction.Begin context when template exists', () => {
- // Arrange
- const { station } = createMockChargingStation({
- ocppVersion: OCPPVersion.VERSION_16,
- stationInfo: { ocppVersion: OCPPVersion.VERSION_16 },
- })
- const connectorStatus = station.getConnectorStatus(1)
- if (connectorStatus != null) {
- connectorStatus.MeterValues = createMeterValuesTemplate([
- {
- measurand: OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
- unit: OCPP16MeterValueUnit.WATT_HOUR,
- value: '0',
- },
- ])
- }
-
- // Act
- const meterValue = OCPP16ServiceUtils.buildTransactionBeginMeterValue(station, 1, 1000)
-
- // Assert
- assert.notStrictEqual(meterValue, undefined)
- assert.ok(meterValue.timestamp instanceof Date)
- assert.strictEqual(Array.isArray(meterValue.sampledValue), true)
- assert.strictEqual(meterValue.sampledValue.length, 1)
- assert.strictEqual(
- meterValue.sampledValue[0].context,
- OCPP16MeterValueContext.TRANSACTION_BEGIN
- )
- })
-
- await it('should apply Wh unit divider of 1 for meterStart', () => {
- // Arrange
- const { station } = createMockChargingStation({
- ocppVersion: OCPPVersion.VERSION_16,
- stationInfo: { ocppVersion: OCPPVersion.VERSION_16 },
- })
- const connectorStatus = station.getConnectorStatus(1)
- if (connectorStatus != null) {
- connectorStatus.MeterValues = createMeterValuesTemplate([
- {
- measurand: OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
- unit: OCPP16MeterValueUnit.WATT_HOUR,
- value: '0',
- },
- ])
- }
-
- // Act
- const meterValue = OCPP16ServiceUtils.buildTransactionBeginMeterValue(station, 1, 5000)
-
- // Assert — Wh divider is 1, so value = 5000 / 1 = 5000
- assert.strictEqual(meterValue.sampledValue[0].value, '5000')
- })
-
- await it('should apply kWh unit divider of 1000 for meterStart', () => {
- // Arrange
- const { station } = createMockChargingStation({
- ocppVersion: OCPPVersion.VERSION_16,
- stationInfo: { ocppVersion: OCPPVersion.VERSION_16 },
- })
- const connectorStatus = station.getConnectorStatus(1)
- if (connectorStatus != null) {
- connectorStatus.MeterValues = createMeterValuesTemplate([
- {
- measurand: OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
- unit: OCPP16MeterValueUnit.KILO_WATT_HOUR,
- value: '0',
- },
- ])
- }
-
- // Act
- const meterValue = OCPP16ServiceUtils.buildTransactionBeginMeterValue(station, 1, 5000)
-
- // Assert — kWh divider is 1000, so value = 5000 / 1000 = 5
- assert.strictEqual(meterValue.sampledValue[0].value, '5')
- })
-
- await it('should use meterStart 0 when undefined', () => {
- // Arrange
- const { station } = createMockChargingStation({
- ocppVersion: OCPPVersion.VERSION_16,
- stationInfo: { ocppVersion: OCPPVersion.VERSION_16 },
- })
- const connectorStatus = station.getConnectorStatus(1)
- if (connectorStatus != null) {
- connectorStatus.MeterValues = createMeterValuesTemplate([
- {
- measurand: OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
- unit: OCPP16MeterValueUnit.WATT_HOUR,
- value: '0',
- },
- ])
- }
-
- // Act
- const meterValue = OCPP16ServiceUtils.buildTransactionBeginMeterValue(station, 1, undefined)
-
- // Assert — undefined meterStart defaults to 0
- assert.strictEqual(meterValue.sampledValue[0].value, '0')
- })
-
- await it('should throw when MeterValues template is empty (missing default measurand)', () => {
- const { station } = createMockChargingStation({
- ocppVersion: OCPPVersion.VERSION_16,
- stationInfo: { ocppVersion: OCPPVersion.VERSION_16 },
- })
-
- assert.throws(
- () => {
- OCPP16ServiceUtils.buildTransactionBeginMeterValue(station, 1, 100)
- },
- { message: /Missing MeterValues for default measurand/ }
- )
- })
- })
-
- // ─── buildTransactionDataMeterValues ───────────────────────────────────
-
- await describe('buildTransactionDataMeterValues', async () => {
- await it('should return array containing both begin and end meter values', () => {
- // Arrange
- const beginMeterValue: OCPP16MeterValue = {
- sampledValue: [{ context: OCPP16MeterValueContext.TRANSACTION_BEGIN, value: '0' }],
- timestamp: new Date('2025-01-01T00:00:00Z'),
- } as OCPP16MeterValue
- const endMeterValue: OCPP16MeterValue = {
- sampledValue: [{ context: OCPP16MeterValueContext.TRANSACTION_END, value: '100' }],
- timestamp: new Date('2025-01-01T01:00:00Z'),
- } as OCPP16MeterValue
-
- // Act
- const result = OCPP16ServiceUtils.buildTransactionDataMeterValues(
- beginMeterValue,
- endMeterValue
- )
-
- // Assert
- assert.strictEqual(result.length, 2)
- assert.strictEqual(result[0], beginMeterValue)
- assert.strictEqual(result[1], endMeterValue)
- })
-
- await it('should return a new array instance', () => {
- const beginMeterValue: OCPP16MeterValue = {
- sampledValue: [],
- timestamp: new Date(),
- } as OCPP16MeterValue
- const endMeterValue: OCPP16MeterValue = {
- sampledValue: [],
- timestamp: new Date(),
- } as OCPP16MeterValue
-
- const result1 = OCPP16ServiceUtils.buildTransactionDataMeterValues(
- beginMeterValue,
- endMeterValue
- )
- const result2 = OCPP16ServiceUtils.buildTransactionDataMeterValues(
- beginMeterValue,
- endMeterValue
- )
-
- // Different array instances
- assert.notStrictEqual(result1, result2)
- })
- })
-
- // ─── buildTransactionEndMeterValue ─────────────────────────────────────
-
- await describe('buildTransactionEndMeterValue', async () => {
- await it('should return a meter value with Transaction.End context', () => {
- // Arrange
- const { station } = createMockChargingStation({
- ocppVersion: OCPPVersion.VERSION_16,
- stationInfo: { ocppVersion: OCPPVersion.VERSION_16 },
- })
- const connectorStatus = station.getConnectorStatus(1)
- if (connectorStatus != null) {
- connectorStatus.MeterValues = createMeterValuesTemplate([
- {
- measurand: OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
- unit: OCPP16MeterValueUnit.WATT_HOUR,
- value: '0',
- },
- ])
- }
-
- // Act
- const meterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(station, 1, 10000)
-
- // Assert
- assert.notStrictEqual(meterValue, undefined)
- assert.ok(meterValue.timestamp instanceof Date)
- assert.strictEqual(meterValue.sampledValue.length, 1)
- assert.strictEqual(
- meterValue.sampledValue[0].context,
- OCPP16MeterValueContext.TRANSACTION_END
- )
- })
-
- await it('should apply kWh unit divider for end meter value', () => {
- // Arrange
- const { station } = createMockChargingStation({
- ocppVersion: OCPPVersion.VERSION_16,
- stationInfo: { ocppVersion: OCPPVersion.VERSION_16 },
- })
- const connectorStatus = station.getConnectorStatus(1)
- if (connectorStatus != null) {
- connectorStatus.MeterValues = createMeterValuesTemplate([
- {
- measurand: OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
- unit: OCPP16MeterValueUnit.KILO_WATT_HOUR,
- value: '0',
- },
- ])
- }
-
- // Act
- const meterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(station, 1, 3000)
-
- // Assert — kWh divider: 3000 / 1000 = 3
- assert.strictEqual(meterValue.sampledValue[0].value, '3')
- })
- })
-
// ─── clearChargingProfiles ──────────────────────────────────────────────
await describe('clearChargingProfiles', async () => {
// Assert
assert.strictEqual(result.length, 0)
})
+
+ await it('should return empty array when EVSE has no MeterValues template', () => {
+ // Arrange
+ const transactionId = generateUUID()
+ const connectorStatus = station.getConnectorStatus(1)
+ if (connectorStatus != null) {
+ connectorStatus.transactionId = transactionId
+ connectorStatus.transactionStarted = true
+ connectorStatus.transactionEnergyActiveImportRegisterValue = 0
+ }
+
+ addConfigurationKey(
+ station,
+ `${OCPP20ComponentName.SampledDataCtrlr}.${OCPP20RequiredVariableName.TxStartedMeasurands}`,
+ 'Energy.Active.Import.Register',
+ undefined,
+ { save: false }
+ )
+
+ // Act
+ const result = OCPP20ServiceUtils.buildTransactionStartedMeterValues(station, transactionId)
+
+ // Assert
+ assert.strictEqual(result.length, 0)
+ })
})
await describe('buildTransactionEndedMeterValues', async () => {
import type { ChargingStation } from '../../../src/charging-station/index.js'
+import { addConfigurationKey } from '../../../src/charging-station/index.js'
import { buildMeterValue } from '../../../src/charging-station/ocpp/OCPPServiceUtils.js'
import {
+ MeterValueContext,
MeterValueMeasurand,
OCPPVersion,
type SampledValueTemplate,
+ StandardParametersKey,
} from '../../../src/types/index.js'
import { Constants } from '../../../src/utils/index.js'
import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
value: '0',
} as unknown as SampledValueTemplate
+const socTemplate: SampledValueTemplate = {
+ measurand: MeterValueMeasurand.STATE_OF_CHARGE,
+ unit: 'Percent',
+ value: '75',
+} as unknown as SampledValueTemplate
+
+const voltageTemplate: SampledValueTemplate = {
+ measurand: MeterValueMeasurand.VOLTAGE,
+ unit: 'V',
+ value: '230',
+} as unknown as SampledValueTemplate
+
+const powerTemplate: SampledValueTemplate = {
+ measurand: MeterValueMeasurand.POWER_ACTIVE_IMPORT,
+ unit: 'W',
+ value: '11000',
+} as unknown as SampledValueTemplate
+
+const currentTemplate: SampledValueTemplate = {
+ measurand: MeterValueMeasurand.CURRENT_IMPORT,
+ unit: 'A',
+ value: '16',
+} as unknown as SampledValueTemplate
+
await describe('buildMeterValue', async () => {
let station: ChargingStation
)
})
})
+
+ // Builder output tests — version-parameterized
+ const VERSIONS = [
+ { transactionId: TEST_TRANSACTION_ID, useEvses: false, version: OCPPVersion.VERSION_16 },
+ { transactionId: TEST_TRANSACTION_ID_STRING, useEvses: true, version: OCPPVersion.VERSION_201 },
+ ] as const
+
+ for (const { transactionId, useEvses, version } of VERSIONS) {
+ await describe(`builder output — ${version}`, async () => {
+ const createStation = (templates: SampledValueTemplate[]): ChargingStation => {
+ const opts = useEvses
+ ? {
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 1,
+ evseConfiguration: { evsesCount: 1 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: { ocppVersion: version },
+ websocketPingInterval: Constants.DEFAULT_WS_PING_INTERVAL,
+ }
+ : {
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 1,
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: { ocppVersion: version },
+ websocketPingInterval: Constants.DEFAULT_WS_PING_INTERVAL,
+ }
+ const { station: s } = createMockChargingStation(opts)
+ const connectorStatus = s.getConnectorStatus(1)
+ if (connectorStatus != null) {
+ connectorStatus.MeterValues = templates
+ connectorStatus.transactionId = transactionId
+ }
+ return s
+ }
+
+ await it('should propagate context to all sampled values', () => {
+ // Arrange
+ const s = createStation([energyTemplate])
+
+ // Act
+ const meterValue = buildMeterValue(
+ s,
+ transactionId,
+ 0,
+ undefined,
+ MeterValueContext.TRANSACTION_BEGIN
+ )
+
+ // Assert
+ for (const sampledValue of meterValue.sampledValue) {
+ assert.strictEqual(sampledValue.context, MeterValueContext.TRANSACTION_BEGIN)
+ }
+ })
+
+ await it('should build sampled values in correct measurand order', () => {
+ // Arrange
+ const s = createStation([
+ socTemplate,
+ voltageTemplate,
+ powerTemplate,
+ currentTemplate,
+ energyTemplate,
+ ])
+ addConfigurationKey(
+ s,
+ StandardParametersKey.MeterValuesSampledData,
+ 'SoC,Voltage,Power.Active.Import,Current.Import,Energy.Active.Import.Register'
+ )
+
+ // Act
+ const meterValue = buildMeterValue(s, transactionId, 0)
+
+ // Assert
+ const measurands = meterValue.sampledValue.map(sv => sv.measurand)
+ const socIdx = measurands.indexOf(MeterValueMeasurand.STATE_OF_CHARGE)
+ const voltageIdx = measurands.indexOf(MeterValueMeasurand.VOLTAGE)
+ const powerIdx = measurands.indexOf(MeterValueMeasurand.POWER_ACTIVE_IMPORT)
+ const currentIdx = measurands.indexOf(MeterValueMeasurand.CURRENT_IMPORT)
+ const energyIdx = measurands.indexOf(MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER)
+ assert.ok(socIdx < voltageIdx, 'SoC should come before Voltage')
+ assert.ok(voltageIdx < powerIdx, 'Voltage should come before Power')
+ assert.ok(powerIdx < currentIdx, 'Power should come before Current')
+ assert.ok(currentIdx < energyIdx, 'Current should come before Energy')
+ })
+
+ await it('should produce version-specific sampled value format', () => {
+ // Arrange
+ const s = createStation([energyTemplate])
+
+ // Act
+ const meterValue = buildMeterValue(s, transactionId, 0)
+
+ // Assert
+ assert.ok(meterValue.sampledValue.length > 0, 'should have at least one sampled value')
+ const sampledValue = meterValue.sampledValue[0]
+ if (version === OCPPVersion.VERSION_16) {
+ assert.strictEqual(typeof sampledValue.value, 'string')
+ assert.ok('unit' in sampledValue)
+ } else {
+ assert.strictEqual(typeof sampledValue.value, 'number')
+ assert.ok('unitOfMeasure' in sampledValue)
+ }
+ })
+ })
+ }
})