From 8cd043999d9d1717723f0ec0a7261d05513bbc10 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Wed, 5 Nov 2025 00:30:03 +0100 Subject: [PATCH] refactor: factor out meter values generation code (#1585) MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit * refactor: factor out meter values generation code Signed-off-by: Jérôme Benoit * refactor: use helpers in both ocpp version code path Signed-off-by: Jérôme Benoit * refactor: align helpers namespace Signed-off-by: Jérôme Benoit * refactor: consolidate type definitions Signed-off-by: Jérôme Benoit * refactor: cleanup meter values payload defaults handling Signed-off-by: Jérôme Benoit * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Signed-off-by: Jérôme Benoit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/charging-station/ocpp/OCPPConstants.ts | 6 +- src/charging-station/ocpp/OCPPServiceUtils.ts | 2032 +++++++++-------- 2 files changed, 1092 insertions(+), 946 deletions(-) diff --git a/src/charging-station/ocpp/OCPPConstants.ts b/src/charging-station/ocpp/OCPPConstants.ts index 6f530edf..a8b9b311 100644 --- a/src/charging-station/ocpp/OCPPConstants.ts +++ b/src/charging-station/ocpp/OCPPConstants.ts @@ -73,11 +73,11 @@ export class OCPPConstants { }) static readonly OCPP_MEASURANDS_SUPPORTED = Object.freeze([ - MeterValueMeasurand.STATE_OF_CHARGE, - MeterValueMeasurand.VOLTAGE, + MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER, MeterValueMeasurand.POWER_ACTIVE_IMPORT, MeterValueMeasurand.CURRENT_IMPORT, - MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER, + MeterValueMeasurand.VOLTAGE, + MeterValueMeasurand.STATE_OF_CHARGE, ]) static readonly OCPP_REQUEST_EMPTY = Constants.EMPTY_FROZEN_OBJECT diff --git a/src/charging-station/ocpp/OCPPServiceUtils.ts b/src/charging-station/ocpp/OCPPServiceUtils.ts index 83288e9b..df70b9b1 100644 --- a/src/charging-station/ocpp/OCPPServiceUtils.ts +++ b/src/charging-station/ocpp/OCPPServiceUtils.ts @@ -71,6 +71,17 @@ import { OCPP16Constants } from './1.6/OCPP16Constants.js' import { OCPP20Constants } from './2.0/OCPP20Constants.js' import { OCPPConstants } from './OCPPConstants.js' +interface MultiPhaseMeasurandData { + perPhaseTemplates: MeasurandPerPhaseSampledValueTemplates + template: SampledValueTemplate + values: MeasurandValues +} + +interface SingleValueMeasurandData { + template: SampledValueTemplate + value: number +} + export const getMessageTypeString = (messageType: MessageType | undefined): string => { switch (messageType) { case MessageType.CALL_ERROR_MESSAGE: @@ -326,788 +337,970 @@ export const convertDateToISOString = (object: T): void => { } } -export const buildMeterValue = ( +const buildSocMeasurandValue = ( + chargingStation: ChargingStation, + connectorId: number +): null | SingleValueMeasurandData => { + const socSampledValueTemplate = getSampledValueTemplate( + chargingStation, + connectorId, + MeterValueMeasurand.STATE_OF_CHARGE + ) + if (socSampledValueTemplate == null) { + return 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) + + return { + template: socSampledValueTemplate, + value: socSampledValueTemplateValue, + } +} + +const validateSocMeasurandValue = ( chargingStation: ChargingStation, connectorId: number, - transactionId: number, - interval: number, - debug = false -): MeterValue => { + sampledValue: SampledValue, + socMinimumValue: number, + socMaximumValue: number, + debug: boolean +): void => { const connector = chargingStation.getConnectorStatus(connectorId) - let meterValue: MeterValue - let connectorMaximumAvailablePower: number | undefined - let socSampledValueTemplate: SampledValueTemplate | undefined - let voltageSampledValueTemplate: SampledValueTemplate | undefined - let powerSampledValueTemplate: SampledValueTemplate | undefined - let powerPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {} - let currentSampledValueTemplate: SampledValueTemplate | undefined - let currentPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {} - let energySampledValueTemplate: SampledValueTemplate | undefined - switch (chargingStation.stationInfo?.ocppVersion) { - case OCPPVersion.VERSION_16: - meterValue = { - sampledValue: [], - timestamp: new Date(), - } - // SoC measurand - socSampledValueTemplate = getSampledValueTemplate( + if ( + convertToInt(sampledValue.value) > socMaximumValue || + convertToInt(sampledValue.value) < socMinimumValue || + debug + ) { + logger.error( + `${chargingStation.logPrefix()} MeterValues measurand ${ + sampledValue.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()}/${sampledValue.value.toString()}/${socMaximumValue.toString()}` + ) + } +} + +const buildVoltageMeasurandValue = ( + chargingStation: ChargingStation, + connectorId: number +): null | SingleValueMeasurandData => { + const voltageSampledValueTemplate = getSampledValueTemplate( + chargingStation, + connectorId, + MeterValueMeasurand.VOLTAGE + ) + if (voltageSampledValueTemplate == null) { + return 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 + ) + + return { + template: voltageSampledValueTemplate, + value: voltageMeasurandValue, + } +} + +const addMainVoltageToMeterValue = ( + chargingStation: ChargingStation, + meterValue: MeterValue, + voltageData: { template: SampledValueTemplate; value: number } +): void => { + if ( + chargingStation.getNumberOfPhases() !== 3 || + (chargingStation.getNumberOfPhases() === 3 && + chargingStation.stationInfo.mainVoltageMeterValues === true) + ) { + meterValue.sampledValue.push( + buildSampledValue( + chargingStation.stationInfo.ocppVersion, + voltageData.template, + voltageData.value + ) + ) + } +} + +const addPhaseVoltageToMeterValue = ( + chargingStation: ChargingStation, + connectorId: number, + meterValue: MeterValue, + mainVoltageData: { template: SampledValueTemplate; value: number }, + phase: number +): void => { + const phaseLineToNeutralValue = `L${phase.toString()}-N` as MeterValuePhase + const voltagePhaseLineToNeutralSampledValueTemplate = getSampledValueTemplate( + chargingStation, + connectorId, + MeterValueMeasurand.VOLTAGE, + phaseLineToNeutralValue + ) + 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 ?? mainVoltageData.template, + voltagePhaseLineToNeutralMeasurandValue ?? mainVoltageData.value, + undefined, + phaseLineToNeutralValue + ) + ) +} + +const addLineToLineVoltageToMeterValue = ( + chargingStation: ChargingStation, + connectorId: number, + meterValue: MeterValue, + mainVoltageData: { template: SampledValueTemplate; value: number }, + phase: number +): void => { + if (chargingStation.stationInfo.phaseLineToLineVoltageMeterValues === true) { + const phaseLineToLineValue = `L${phase.toString()}-L${ + (phase + 1) % chargingStation.getNumberOfPhases() !== 0 + ? ((phase + 1) % chargingStation.getNumberOfPhases()).toString() + : chargingStation.getNumberOfPhases().toString() + }` as MeterValuePhase + 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 + ) + 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 + ) + } + meterValue.sampledValue.push( + buildSampledValue( + chargingStation.stationInfo.ocppVersion, + voltagePhaseLineToLineSampledValueTemplate ?? mainVoltageData.template, + voltagePhaseLineToLineMeasurandValue ?? voltagePhaseLineToLineValueRounded, + undefined, + phaseLineToLineValue + ) + ) + } +} + +const buildEnergyMeasurandValue = ( + chargingStation: ChargingStation, + connectorId: number, + interval: number +): null | SingleValueMeasurandData => { + const energyTemplate = getSampledValueTemplate(chargingStation, connectorId) + if (energyTemplate == null) { + return null + } + + checkMeasurandPowerDivider(chargingStation, energyTemplate.measurand) + const unitDivider = energyTemplate.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1 + const connectorMaximumAvailablePower = + chargingStation.getConnectorMaximumAvailablePower(connectorId) + const connectorMaximumEnergyRounded = roundTo( + (connectorMaximumAvailablePower * interval) / (3600 * 1000), + 2 + ) + const connectorMinimumEnergyRounded = roundTo(energyTemplate.minimumValue ?? 0, 2) + + const energyValueRounded = isNotEmptyString(energyTemplate.value) + ? getRandomFloatFluctuatedRounded( + getLimitFromSampledValueTemplateCustomValue( + energyTemplate.value, + connectorMaximumEnergyRounded, + connectorMinimumEnergyRounded, + { + fallbackValue: connectorMinimumEnergyRounded, + limitationEnabled: chargingStation.stationInfo?.customValueLimitationMeterValues, + unitMultiplier: unitDivider, + } + ), + energyTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT + ) + : getRandomFloatRounded(connectorMaximumEnergyRounded, connectorMinimumEnergyRounded) + + return { + template: energyTemplate, + value: energyValueRounded, + } +} + +const updateConnectorEnergyValues = ( + connector: ConnectorStatus | undefined, + energyValue: number +): void => { + if (connector != null) { + if ( + connector.energyActiveImportRegisterValue != null && + connector.energyActiveImportRegisterValue >= 0 && + connector.transactionEnergyActiveImportRegisterValue != null && + connector.transactionEnergyActiveImportRegisterValue >= 0 + ) { + connector.energyActiveImportRegisterValue += energyValue + connector.transactionEnergyActiveImportRegisterValue += energyValue + } else { + connector.energyActiveImportRegisterValue = 0 + connector.transactionEnergyActiveImportRegisterValue = 0 + } + } +} + +const validateEnergyMeasurandValue = ( + chargingStation: ChargingStation, + connectorId: number, + sampledValue: SampledValue, + energyValue: number, + minValue: number, + maxValue: number, + interval: number, + debug: boolean +): void => { + if (energyValue > maxValue || energyValue < minValue || debug) { + const connector = chargingStation.getConnectorStatus(connectorId) + logger.error( + `${chargingStation.logPrefix()} MeterValues measurand ${ + sampledValue.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: ${minValue.toString()}/${energyValue.toString()}/${maxValue.toString()}, duration: ${interval.toString()}ms` + ) + } +} + +const buildPowerMeasurandValue = ( + chargingStation: ChargingStation, + connectorId: number +): MultiPhaseMeasurandData | null => { + const powerTemplate = getSampledValueTemplate( + chargingStation, + connectorId, + MeterValueMeasurand.POWER_ACTIVE_IMPORT + ) + if (powerTemplate == null) { + return null + } + + let perPhaseTemplates: MeasurandPerPhaseSampledValueTemplates = {} + if (chargingStation.getNumberOfPhases() === 3) { + perPhaseTemplates = { + L1: getSampledValueTemplate( chargingStation, connectorId, - MeterValueMeasurand.STATE_OF_CHARGE - ) - if (socSampledValueTemplate != null) { - const socMaximumValue = 100 - const socMinimumValue = socSampledValueTemplate.minimumValue ?? 0 - const socSampledValueTemplateValue = isNotEmptyString(socSampledValueTemplate.value) + MeterValueMeasurand.POWER_ACTIVE_IMPORT, + MeterValuePhase.L1_N + ), + L2: getSampledValueTemplate( + chargingStation, + connectorId, + MeterValueMeasurand.POWER_ACTIVE_IMPORT, + MeterValuePhase.L2_N + ), + L3: getSampledValueTemplate( + chargingStation, + connectorId, + MeterValueMeasurand.POWER_ACTIVE_IMPORT, + MeterValuePhase.L3_N + ), + } + } + + checkMeasurandPowerDivider(chargingStation, powerTemplate.measurand) + const powerValues: MeasurandValues = {} as MeasurandValues + const unitDivider = powerTemplate.unit === MeterValueUnit.KILO_WATT ? 1000 : 1 + const connectorMaximumAvailablePower = + chargingStation.getConnectorMaximumAvailablePower(connectorId) + const connectorMaximumPower = Math.round(connectorMaximumAvailablePower) + const connectorMaximumPowerPerPhase = Math.round( + connectorMaximumAvailablePower / chargingStation.getNumberOfPhases() + ) + const connectorMinimumPower = Math.round(powerTemplate.minimumValue ?? 0) + const connectorMinimumPowerPerPhase = Math.round( + connectorMinimumPower / chargingStation.getNumberOfPhases() + ) + + switch (chargingStation.stationInfo?.currentOutType) { + case CurrentType.AC: + if (chargingStation.getNumberOfPhases() === 3) { + const defaultFluctuatedPowerPerPhase = isNotEmptyString(powerTemplate.value) ? getRandomFloatFluctuatedRounded( - Number.parseInt(socSampledValueTemplate.value), - socSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT + getLimitFromSampledValueTemplateCustomValue( + powerTemplate.value, + connectorMaximumPower / unitDivider, + connectorMinimumPower / unitDivider, + { + fallbackValue: connectorMinimumPower / unitDivider, + limitationEnabled: chargingStation.stationInfo.customValueLimitationMeterValues, + } + ) / chargingStation.getNumberOfPhases(), + powerTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) - : randomInt(socMinimumValue, socMaximumValue + 1) - meterValue.sampledValue.push( - buildSampledValue( - chargingStation.stationInfo.ocppVersion, - socSampledValueTemplate, - socSampledValueTemplateValue + : undefined + + const phase1Value = isNotEmptyString(perPhaseTemplates.L1?.value) + ? getRandomFloatFluctuatedRounded( + getLimitFromSampledValueTemplateCustomValue( + perPhaseTemplates.L1.value, + connectorMaximumPowerPerPhase / unitDivider, + connectorMinimumPowerPerPhase / unitDivider, + { + fallbackValue: connectorMinimumPowerPerPhase / unitDivider, + limitationEnabled: chargingStation.stationInfo.customValueLimitationMeterValues, + } + ), + perPhaseTemplates.L1.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) - ) - 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()}` + : undefined + + const phase2Value = isNotEmptyString(perPhaseTemplates.L2?.value) + ? getRandomFloatFluctuatedRounded( + getLimitFromSampledValueTemplateCustomValue( + perPhaseTemplates.L2.value, + connectorMaximumPowerPerPhase / unitDivider, + connectorMinimumPowerPerPhase / unitDivider, + { + fallbackValue: connectorMinimumPowerPerPhase / unitDivider, + limitationEnabled: chargingStation.stationInfo.customValueLimitationMeterValues, + } + ), + perPhaseTemplates.L2.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) - } + : undefined + + const phase3Value = isNotEmptyString(perPhaseTemplates.L3?.value) + ? getRandomFloatFluctuatedRounded( + getLimitFromSampledValueTemplateCustomValue( + perPhaseTemplates.L3.value, + connectorMaximumPowerPerPhase / unitDivider, + connectorMinimumPowerPerPhase / unitDivider, + { + fallbackValue: connectorMinimumPowerPerPhase / unitDivider, + limitationEnabled: chargingStation.stationInfo.customValueLimitationMeterValues, + } + ), + perPhaseTemplates.L3.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT + ) + : undefined + + powerValues.L1 = + phase1Value ?? + defaultFluctuatedPowerPerPhase ?? + getRandomFloatRounded( + connectorMaximumPowerPerPhase / unitDivider, + connectorMinimumPowerPerPhase / unitDivider + ) + powerValues.L2 = + phase2Value ?? + defaultFluctuatedPowerPerPhase ?? + getRandomFloatRounded( + connectorMaximumPowerPerPhase / unitDivider, + connectorMinimumPowerPerPhase / unitDivider + ) + powerValues.L3 = + phase3Value ?? + defaultFluctuatedPowerPerPhase ?? + getRandomFloatRounded( + connectorMaximumPowerPerPhase / unitDivider, + connectorMinimumPowerPerPhase / unitDivider + ) + } else { + powerValues.L1 = isNotEmptyString(powerTemplate.value) + ? getRandomFloatFluctuatedRounded( + getLimitFromSampledValueTemplateCustomValue( + powerTemplate.value, + connectorMaximumPower / unitDivider, + connectorMinimumPower / unitDivider, + { + fallbackValue: connectorMinimumPower / unitDivider, + limitationEnabled: chargingStation.stationInfo.customValueLimitationMeterValues, + } + ), + powerTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT + ) + : getRandomFloatRounded( + connectorMaximumPower / unitDivider, + connectorMinimumPower / unitDivider + ) + powerValues.L2 = 0 + powerValues.L3 = 0 } - // Voltage measurand - voltageSampledValueTemplate = getSampledValueTemplate( + powerValues.allPhases = roundTo(powerValues.L1 + powerValues.L2 + powerValues.L3, 2) + break + case CurrentType.DC: + powerValues.allPhases = isNotEmptyString(powerTemplate.value) + ? getRandomFloatFluctuatedRounded( + getLimitFromSampledValueTemplateCustomValue( + powerTemplate.value, + connectorMaximumPower / unitDivider, + connectorMinimumPower / unitDivider, + { + fallbackValue: connectorMinimumPower / unitDivider, + limitationEnabled: chargingStation.stationInfo.customValueLimitationMeterValues, + } + ), + powerTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT + ) + : getRandomFloatRounded( + connectorMaximumPower / unitDivider, + connectorMinimumPower / unitDivider + ) + break + default: { + const errMsg = `MeterValues measurand ${ + powerTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + }: Unknown ${chargingStation.stationInfo?.currentOutType} currentOutType in template file ${ + chargingStation.templateFile + }, cannot calculate ${ + powerTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER + } measurand value` + logger.error(`${chargingStation.logPrefix()} ${errMsg}`) + throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES) + } + } + + return { + perPhaseTemplates, + template: powerTemplate, + values: powerValues, + } +} + +const validatePowerMeasurandValue = ( + chargingStation: ChargingStation, + connectorId: number, + connector: ConnectorStatus | undefined, + sampledValue: SampledValue, + connectorMaximumPower: number, + connectorMinimumPower: number, + debug: boolean +): void => { + if ( + convertToFloat(sampledValue.value) > connectorMaximumPower || + convertToFloat(sampledValue.value) < connectorMinimumPower || + debug + ) { + logger.error( + `${chargingStation.logPrefix()} MeterValues measurand ${ + sampledValue.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: ${connectorMinimumPower.toString()}/${sampledValue.value.toString()}/${connectorMaximumPower.toString()}` + ) + } +} + +const validateCurrentMeasurandValue = ( + chargingStation: ChargingStation, + connectorId: number, + connector: ConnectorStatus | undefined, + sampledValue: SampledValue, + connectorMaximumAmperage: number, + connectorMinimumAmperage: number, + debug: boolean +): void => { + if ( + convertToFloat(sampledValue.value) > connectorMaximumAmperage || + convertToFloat(sampledValue.value) < connectorMinimumAmperage || + debug + ) { + logger.error( + `${chargingStation.logPrefix()} MeterValues measurand ${ + sampledValue.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()}/${sampledValue.value.toString()}/${connectorMaximumAmperage.toString()}` + ) + } +} + +const validateCurrentMeasurandPhaseValue = ( + chargingStation: ChargingStation, + connectorId: number, + connector: ConnectorStatus | undefined, + sampledValue: SampledValue, + connectorMaximumAmperage: number, + connectorMinimumAmperage: number, + debug: boolean +): void => { + if ( + convertToFloat(sampledValue.value) > connectorMaximumAmperage || + convertToFloat(sampledValue.value) < connectorMinimumAmperage || + debug + ) { + logger.error( + `${chargingStation.logPrefix()} MeterValues measurand ${ + sampledValue.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER + }: phase ${ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + sampledValue.phase + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + }, connector id ${connectorId.toString()}, transaction id ${connector?.transactionId?.toString()}, value: ${connectorMinimumAmperage.toString()}/${sampledValue.value.toString()}/${connectorMaximumAmperage.toString()}` + ) + } +} + +const buildCurrentMeasurandValue = ( + chargingStation: ChargingStation, + connectorId: number +): MultiPhaseMeasurandData | null => { + const currentTemplate = getSampledValueTemplate( + chargingStation, + connectorId, + MeterValueMeasurand.CURRENT_IMPORT + ) + if (currentTemplate == null) { + return null + } + + let perPhaseTemplates: MeasurandPerPhaseSampledValueTemplates = {} + if (chargingStation.getNumberOfPhases() === 3) { + perPhaseTemplates = { + L1: getSampledValueTemplate( + chargingStation, + connectorId, + MeterValueMeasurand.CURRENT_IMPORT, + MeterValuePhase.L1 + ), + L2: getSampledValueTemplate( chargingStation, connectorId, - MeterValueMeasurand.VOLTAGE + MeterValueMeasurand.CURRENT_IMPORT, + MeterValuePhase.L2 + ), + L3: getSampledValueTemplate( + chargingStation, + connectorId, + MeterValueMeasurand.CURRENT_IMPORT, + MeterValuePhase.L3 + ), + } + } + + checkMeasurandPowerDivider(chargingStation, currentTemplate.measurand) + const currentValues: MeasurandValues = {} as MeasurandValues + const connectorMaximumAvailablePower = + chargingStation.getConnectorMaximumAvailablePower(connectorId) + const connectorMinimumAmperage = currentTemplate.minimumValue ?? 0 + let connectorMaximumAmperage: number + + switch (chargingStation.stationInfo?.currentOutType) { + case CurrentType.AC: + connectorMaximumAmperage = ACElectricUtils.amperagePerPhaseFromPower( + chargingStation.getNumberOfPhases(), + connectorMaximumAvailablePower, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + chargingStation.stationInfo.voltageOut! ) - 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 - ) + if (chargingStation.getNumberOfPhases() === 3) { + const defaultFluctuatedAmperagePerPhase = isNotEmptyString(currentTemplate.value) + ? getRandomFloatFluctuatedRounded( + getLimitFromSampledValueTemplateCustomValue( + currentTemplate.value, + connectorMaximumAmperage, + connectorMinimumAmperage, + { + fallbackValue: connectorMinimumAmperage, + limitationEnabled: chargingStation.stationInfo.customValueLimitationMeterValues, + } + ), + currentTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) - } - 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 + : undefined + + const phase1Value = isNotEmptyString(perPhaseTemplates.L1?.value) + ? getRandomFloatFluctuatedRounded( + getLimitFromSampledValueTemplateCustomValue( + perPhaseTemplates.L1.value, + connectorMaximumAmperage, + connectorMinimumAmperage, + { + fallbackValue: connectorMinimumAmperage, + limitationEnabled: chargingStation.stationInfo.customValueLimitationMeterValues, + } + ), + perPhaseTemplates.L1.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) - 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 - ) + : undefined + + const phase2Value = isNotEmptyString(perPhaseTemplates.L2?.value) + ? getRandomFloatFluctuatedRounded( + getLimitFromSampledValueTemplateCustomValue( + perPhaseTemplates.L2.value, + connectorMaximumAmperage, + connectorMinimumAmperage, + { + fallbackValue: connectorMinimumAmperage, + limitationEnabled: chargingStation.stationInfo.customValueLimitationMeterValues, + } + ), + perPhaseTemplates.L2.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) - 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 - ) + : undefined + + const phase3Value = isNotEmptyString(perPhaseTemplates.L3?.value) + ? getRandomFloatFluctuatedRounded( + getLimitFromSampledValueTemplateCustomValue( + perPhaseTemplates.L3.value, + connectorMaximumAmperage, + connectorMinimumAmperage, + { + fallbackValue: connectorMinimumAmperage, + limitationEnabled: chargingStation.stationInfo.customValueLimitationMeterValues, + } + ), + perPhaseTemplates.L3.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT + ) + : undefined + + currentValues.L1 = + phase1Value ?? + defaultFluctuatedAmperagePerPhase ?? + getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage) + currentValues.L2 = + phase2Value ?? + defaultFluctuatedAmperagePerPhase ?? + getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage) + currentValues.L3 = + phase3Value ?? + defaultFluctuatedAmperagePerPhase ?? + getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage) + } else { + currentValues.L1 = isNotEmptyString(currentTemplate.value) + ? getRandomFloatFluctuatedRounded( + getLimitFromSampledValueTemplateCustomValue( + currentTemplate.value, + connectorMaximumAmperage, + connectorMinimumAmperage, + { + fallbackValue: connectorMinimumAmperage, + limitationEnabled: chargingStation.stationInfo.customValueLimitationMeterValues, + } + ), + currentTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT + ) + : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage) + currentValues.L2 = 0 + currentValues.L3 = 0 + } + currentValues.allPhases = roundTo( + (currentValues.L1 + currentValues.L2 + currentValues.L3) / + chargingStation.getNumberOfPhases(), + 2 + ) + break + case CurrentType.DC: + connectorMaximumAmperage = DCElectricUtils.amperage( + connectorMaximumAvailablePower, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + chargingStation.stationInfo.voltageOut! + ) + currentValues.allPhases = isNotEmptyString(currentTemplate.value) + ? getRandomFloatFluctuatedRounded( + getLimitFromSampledValueTemplateCustomValue( + currentTemplate.value, + connectorMaximumAmperage, + connectorMinimumAmperage, + { + fallbackValue: connectorMinimumAmperage, + limitationEnabled: chargingStation.stationInfo.customValueLimitationMeterValues, } - const defaultVoltagePhaseLineToLineMeasurandValue = getRandomFloatFluctuatedRounded( - voltagePhaseLineToLineValueRounded, - fluctuationPercent - ) - meterValue.sampledValue.push( - buildSampledValue( - chargingStation.stationInfo.ocppVersion, - voltagePhaseLineToLineSampledValueTemplate ?? voltageSampledValueTemplate, - voltagePhaseLineToLineMeasurandValue ?? defaultVoltagePhaseLineToLineMeasurandValue, - undefined, - phaseLineToLineValue as MeterValuePhase - ) - ) - } - } + ), + currentTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT + ) + : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage) + break + default: { + const errMsg = `MeterValues measurand ${ + currentTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + }: Unknown ${chargingStation.stationInfo?.currentOutType} currentOutType in template file ${ + chargingStation.templateFile + }, cannot calculate ${ + currentTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER + } measurand value` + logger.error(`${chargingStation.logPrefix()} ${errMsg}`) + throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES) + } + } + + return { + perPhaseTemplates, + template: currentTemplate, + values: currentValues, + } +} + +export const buildMeterValue = ( + chargingStation: ChargingStation, + connectorId: number, + transactionId: number, + interval: number, + debug = false +): MeterValue => { + const connector = chargingStation.getConnectorStatus(connectorId) + let meterValue: MeterValue + + switch (chargingStation.stationInfo?.ocppVersion) { + case OCPPVersion.VERSION_16: { + meterValue = { + sampledValue: [], + timestamp: new Date(), } - // Power.Active.Import measurand - powerSampledValueTemplate = getSampledValueTemplate( - chargingStation, - connectorId, - MeterValueMeasurand.POWER_ACTIVE_IMPORT - ) - if (chargingStation.getNumberOfPhases() === 3) { - powerPerPhaseSampledValueTemplates = { - L1: getSampledValueTemplate( - chargingStation, - connectorId, - MeterValueMeasurand.POWER_ACTIVE_IMPORT, - MeterValuePhase.L1_N - ), - L2: getSampledValueTemplate( + // SoC measurand + const socMeasurand = buildSocMeasurandValue(chargingStation, connectorId) + if (socMeasurand != null) { + const socSampledValue = buildSampledValue( + chargingStation.stationInfo.ocppVersion, + socMeasurand.template, + socMeasurand.value + ) + meterValue.sampledValue.push(socSampledValue) + validateSocMeasurandValue( + chargingStation, + connectorId, + socSampledValue, + socMeasurand.template.minimumValue ?? 0, + 100, + debug + ) + } + // Voltage measurand + const voltageMeasurand = buildVoltageMeasurandValue(chargingStation, connectorId) + if (voltageMeasurand != null) { + addMainVoltageToMeterValue(chargingStation, meterValue, voltageMeasurand) + for ( + let phase = 1; + chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases(); + phase++ + ) { + addPhaseVoltageToMeterValue( chargingStation, connectorId, - MeterValueMeasurand.POWER_ACTIVE_IMPORT, - MeterValuePhase.L2_N - ), - L3: getSampledValueTemplate( + meterValue, + voltageMeasurand, + phase + ) + addLineToLineVoltageToMeterValue( chargingStation, connectorId, - MeterValueMeasurand.POWER_ACTIVE_IMPORT, - MeterValuePhase.L3_N - ), + meterValue, + voltageMeasurand, + phase + ) } } - if (powerSampledValueTemplate != null) { - checkMeasurandPowerDivider(chargingStation, powerSampledValueTemplate.measurand) - const errMsg = `MeterValues measurand ${ - powerSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - }: Unknown ${chargingStation.stationInfo.currentOutType} currentOutType in template file ${ - chargingStation.templateFile - }, cannot calculate ${ - powerSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER - } measurand value` - const powerMeasurandValues: MeasurandValues = {} as MeasurandValues - const unitDivider = powerSampledValueTemplate.unit === MeterValueUnit.KILO_WATT ? 1000 : 1 - connectorMaximumAvailablePower = + // Power.Active.Import measurand + const powerMeasurand = buildPowerMeasurandValue(chargingStation, connectorId) + if (powerMeasurand != null) { + const unitDivider = powerMeasurand.template.unit === MeterValueUnit.KILO_WATT ? 1000 : 1 + const connectorMaximumAvailablePower = chargingStation.getConnectorMaximumAvailablePower(connectorId) const connectorMaximumPower = Math.round(connectorMaximumAvailablePower) - const connectorMaximumPowerPerPhase = Math.round( - connectorMaximumAvailablePower / chargingStation.getNumberOfPhases() - ) - const connectorMinimumPower = Math.round(powerSampledValueTemplate.minimumValue ?? 0) - const connectorMinimumPowerPerPhase = Math.round( - connectorMinimumPower / chargingStation.getNumberOfPhases() - ) - switch (chargingStation.stationInfo.currentOutType) { - case CurrentType.AC: - if (chargingStation.getNumberOfPhases() === 3) { - const defaultFluctuatedPowerPerPhase = isNotEmptyString( - powerSampledValueTemplate.value - ) - ? getRandomFloatFluctuatedRounded( - getLimitFromSampledValueTemplateCustomValue( - powerSampledValueTemplate.value, - connectorMaximumPower / unitDivider, - connectorMinimumPower / unitDivider, - { - fallbackValue: connectorMinimumPower / unitDivider, - limitationEnabled: - chargingStation.stationInfo.customValueLimitationMeterValues, - } - ) / chargingStation.getNumberOfPhases(), - powerSampledValueTemplate.fluctuationPercent ?? - Constants.DEFAULT_FLUCTUATION_PERCENT - ) - : undefined - const phase1FluctuatedValue = isNotEmptyString( - powerPerPhaseSampledValueTemplates.L1?.value - ) - ? getRandomFloatFluctuatedRounded( - getLimitFromSampledValueTemplateCustomValue( - powerPerPhaseSampledValueTemplates.L1.value, - connectorMaximumPowerPerPhase / unitDivider, - connectorMinimumPowerPerPhase / unitDivider, - { - fallbackValue: connectorMinimumPowerPerPhase / unitDivider, - limitationEnabled: - chargingStation.stationInfo.customValueLimitationMeterValues, - } - ), - powerPerPhaseSampledValueTemplates.L1.fluctuationPercent ?? - Constants.DEFAULT_FLUCTUATION_PERCENT - ) - : undefined - const phase2FluctuatedValue = isNotEmptyString( - powerPerPhaseSampledValueTemplates.L2?.value - ) - ? getRandomFloatFluctuatedRounded( - getLimitFromSampledValueTemplateCustomValue( - powerPerPhaseSampledValueTemplates.L2.value, - connectorMaximumPowerPerPhase / unitDivider, - connectorMinimumPowerPerPhase / unitDivider, - { - fallbackValue: connectorMinimumPowerPerPhase / unitDivider, - limitationEnabled: - chargingStation.stationInfo.customValueLimitationMeterValues, - } - ), - powerPerPhaseSampledValueTemplates.L2.fluctuationPercent ?? - Constants.DEFAULT_FLUCTUATION_PERCENT - ) - : undefined - const phase3FluctuatedValue = isNotEmptyString( - powerPerPhaseSampledValueTemplates.L3?.value - ) - ? getRandomFloatFluctuatedRounded( - getLimitFromSampledValueTemplateCustomValue( - powerPerPhaseSampledValueTemplates.L3.value, - connectorMaximumPowerPerPhase / unitDivider, - connectorMinimumPowerPerPhase / unitDivider, - { - fallbackValue: connectorMinimumPowerPerPhase / unitDivider, - limitationEnabled: - chargingStation.stationInfo.customValueLimitationMeterValues, - } - ), - powerPerPhaseSampledValueTemplates.L3.fluctuationPercent ?? - Constants.DEFAULT_FLUCTUATION_PERCENT - ) - : undefined - powerMeasurandValues.L1 = - phase1FluctuatedValue ?? - defaultFluctuatedPowerPerPhase ?? - getRandomFloatRounded( - connectorMaximumPowerPerPhase / unitDivider, - connectorMinimumPowerPerPhase / unitDivider - ) - powerMeasurandValues.L2 = - phase2FluctuatedValue ?? - defaultFluctuatedPowerPerPhase ?? - getRandomFloatRounded( - connectorMaximumPowerPerPhase / unitDivider, - connectorMinimumPowerPerPhase / unitDivider - ) - powerMeasurandValues.L3 = - phase3FluctuatedValue ?? - defaultFluctuatedPowerPerPhase ?? - getRandomFloatRounded( - connectorMaximumPowerPerPhase / unitDivider, - connectorMinimumPowerPerPhase / unitDivider - ) - } else { - powerMeasurandValues.L1 = isNotEmptyString(powerSampledValueTemplate.value) - ? getRandomFloatFluctuatedRounded( - getLimitFromSampledValueTemplateCustomValue( - powerSampledValueTemplate.value, - connectorMaximumPower / unitDivider, - connectorMinimumPower / unitDivider, - { - fallbackValue: connectorMinimumPower / unitDivider, - limitationEnabled: - chargingStation.stationInfo.customValueLimitationMeterValues, - } - ), - powerSampledValueTemplate.fluctuationPercent ?? - Constants.DEFAULT_FLUCTUATION_PERCENT - ) - : getRandomFloatRounded( - connectorMaximumPower / unitDivider, - connectorMinimumPower / unitDivider - ) - powerMeasurandValues.L2 = 0 - powerMeasurandValues.L3 = 0 - } - powerMeasurandValues.allPhases = roundTo( - powerMeasurandValues.L1 + powerMeasurandValues.L2 + powerMeasurandValues.L3, - 2 - ) - break - case CurrentType.DC: - powerMeasurandValues.allPhases = isNotEmptyString(powerSampledValueTemplate.value) - ? getRandomFloatFluctuatedRounded( - getLimitFromSampledValueTemplateCustomValue( - powerSampledValueTemplate.value, - connectorMaximumPower / unitDivider, - connectorMinimumPower / unitDivider, - { - fallbackValue: connectorMinimumPower / unitDivider, - limitationEnabled: - chargingStation.stationInfo.customValueLimitationMeterValues, - } - ), - powerSampledValueTemplate.fluctuationPercent ?? - Constants.DEFAULT_FLUCTUATION_PERCENT - ) - : getRandomFloatRounded( - connectorMaximumPower / unitDivider, - connectorMinimumPower / unitDivider - ) - break - default: - logger.error(`${chargingStation.logPrefix()} ${errMsg}`) - throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES) - } + const connectorMinimumPower = Math.round(powerMeasurand.template.minimumValue ?? 0) + meterValue.sampledValue.push( buildSampledValue( chargingStation.stationInfo.ocppVersion, - powerSampledValueTemplate, - powerMeasurandValues.allPhases + powerMeasurand.template, + powerMeasurand.values.allPhases ) ) const sampledValuesIndex = meterValue.sampledValue.length - 1 - const connectorMaximumPowerRounded = roundTo(connectorMaximumPower / unitDivider, 2) - const connectorMinimumPowerRounded = roundTo(connectorMinimumPower / unitDivider, 2) - if ( - convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > - connectorMaximumPowerRounded || - convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) < - connectorMinimumPowerRounded || + validatePowerMeasurandValue( + chargingStation, + connectorId, + connector, + meterValue.sampledValue[sampledValuesIndex], + connectorMaximumPower / unitDivider, + connectorMinimumPower / unitDivider, 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: ${connectorMinimumPowerRounded.toString()}/${meterValue.sampledValue[ - sampledValuesIndex - ].value.toString()}/${connectorMaximumPowerRounded.toString()}` - ) - } - for ( - let phase = 1; - chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases(); - phase++ - ) { - const phaseValue = `L${phase.toString()}-N` - meterValue.sampledValue.push( - buildSampledValue( - chargingStation.stationInfo.ocppVersion, - powerPerPhaseSampledValueTemplates[ - `L${phase.toString()}` as keyof MeasurandPerPhaseSampledValueTemplates - ] ?? powerSampledValueTemplate, - powerMeasurandValues[ - `L${phase.toString()}` as keyof MeasurandPerPhaseSampledValueTemplates - ], - undefined, - phaseValue as MeterValuePhase - ) - ) - const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1 - const connectorMaximumPowerPerPhaseRounded = roundTo( - connectorMaximumPowerPerPhase / unitDivider, - 2 + ) + if (chargingStation.getNumberOfPhases() === 3) { + const connectorMaximumPowerPerPhase = Math.round( + connectorMaximumAvailablePower / chargingStation.getNumberOfPhases() ) - const connectorMinimumPowerPerPhaseRounded = roundTo( - connectorMinimumPowerPerPhase / unitDivider, - 2 + const connectorMinimumPowerPerPhase = Math.round( + connectorMinimumPower / chargingStation.getNumberOfPhases() ) - if ( - convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) > - connectorMaximumPowerPerPhaseRounded || - convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) < - connectorMinimumPowerPerPhaseRounded || - debug - ) { - logger.error( - `${chargingStation.logPrefix()} MeterValues measurand ${ - meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ?? - MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER - }: phase ${ - // 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.toString()}/${connectorMaximumPowerPerPhaseRounded.toString()}` - ) + 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( + buildSampledValue( + chargingStation.stationInfo.ocppVersion, + phaseTemplate, + phasePowerValue, + undefined, + phaseValue + ) + ) + const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1 + validatePowerMeasurandValue( + chargingStation, + connectorId, + connector, + meterValue.sampledValue[sampledValuesPerPhaseIndex], + connectorMaximumPowerPerPhase / unitDivider, + connectorMinimumPowerPerPhase / unitDivider, + debug + ) + } } } } // Current.Import measurand - currentSampledValueTemplate = getSampledValueTemplate( - chargingStation, - connectorId, - MeterValueMeasurand.CURRENT_IMPORT - ) - if (chargingStation.getNumberOfPhases() === 3) { - currentPerPhaseSampledValueTemplates = { - L1: getSampledValueTemplate( - chargingStation, - connectorId, - MeterValueMeasurand.CURRENT_IMPORT, - MeterValuePhase.L1 - ), - L2: getSampledValueTemplate( - chargingStation, - connectorId, - MeterValueMeasurand.CURRENT_IMPORT, - MeterValuePhase.L2 - ), - L3: getSampledValueTemplate( - chargingStation, - connectorId, - MeterValueMeasurand.CURRENT_IMPORT, - MeterValuePhase.L3 - ), - } - } - if (currentSampledValueTemplate != null) { - checkMeasurandPowerDivider(chargingStation, currentSampledValueTemplate.measurand) - const errMsg = `MeterValues measurand ${ - currentSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - }: Unknown ${chargingStation.stationInfo.currentOutType} currentOutType in template file ${ - chargingStation.templateFile - }, cannot calculate ${ - currentSampledValueTemplate.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER - } measurand value` - const currentMeasurandValues: MeasurandValues = {} as MeasurandValues - connectorMaximumAvailablePower == null && - (connectorMaximumAvailablePower = - chargingStation.getConnectorMaximumAvailablePower(connectorId)) - const connectorMinimumAmperage = currentSampledValueTemplate.minimumValue ?? 0 - let connectorMaximumAmperage: number - switch (chargingStation.stationInfo.currentOutType) { - case CurrentType.AC: - connectorMaximumAmperage = ACElectricUtils.amperagePerPhaseFromPower( + const currentMeasurand = buildCurrentMeasurandValue(chargingStation, connectorId) + if (currentMeasurand != null) { + const connectorMaximumAvailablePower = + chargingStation.getConnectorMaximumAvailablePower(connectorId) + const connectorMaximumAmperage = + chargingStation.stationInfo.currentOutType === CurrentType.AC + ? ACElectricUtils.amperagePerPhaseFromPower( chargingStation.getNumberOfPhases(), connectorMaximumAvailablePower, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion chargingStation.stationInfo.voltageOut! ) - if (chargingStation.getNumberOfPhases() === 3) { - const defaultFluctuatedAmperagePerPhase = isNotEmptyString( - currentSampledValueTemplate.value - ) - ? getRandomFloatFluctuatedRounded( - getLimitFromSampledValueTemplateCustomValue( - currentSampledValueTemplate.value, - connectorMaximumAmperage, - connectorMinimumAmperage, - { - fallbackValue: connectorMinimumAmperage, - limitationEnabled: - chargingStation.stationInfo.customValueLimitationMeterValues, - } - ), - currentSampledValueTemplate.fluctuationPercent ?? - Constants.DEFAULT_FLUCTUATION_PERCENT - ) - : undefined - const phase1FluctuatedValue = isNotEmptyString( - currentPerPhaseSampledValueTemplates.L1?.value - ) - ? getRandomFloatFluctuatedRounded( - getLimitFromSampledValueTemplateCustomValue( - currentPerPhaseSampledValueTemplates.L1.value, - connectorMaximumAmperage, - connectorMinimumAmperage, - { - fallbackValue: connectorMinimumAmperage, - limitationEnabled: - chargingStation.stationInfo.customValueLimitationMeterValues, - } - ), - currentPerPhaseSampledValueTemplates.L1.fluctuationPercent ?? - Constants.DEFAULT_FLUCTUATION_PERCENT - ) - : undefined - const phase2FluctuatedValue = isNotEmptyString( - currentPerPhaseSampledValueTemplates.L2?.value - ) - ? getRandomFloatFluctuatedRounded( - getLimitFromSampledValueTemplateCustomValue( - currentPerPhaseSampledValueTemplates.L2.value, - connectorMaximumAmperage, - connectorMinimumAmperage, - { - fallbackValue: connectorMinimumAmperage, - limitationEnabled: - chargingStation.stationInfo.customValueLimitationMeterValues, - } - ), - currentPerPhaseSampledValueTemplates.L2.fluctuationPercent ?? - Constants.DEFAULT_FLUCTUATION_PERCENT - ) - : undefined - const phase3FluctuatedValue = isNotEmptyString( - currentPerPhaseSampledValueTemplates.L3?.value - ) - ? getRandomFloatFluctuatedRounded( - getLimitFromSampledValueTemplateCustomValue( - currentPerPhaseSampledValueTemplates.L3.value, - connectorMaximumAmperage, - connectorMinimumAmperage, - { - fallbackValue: connectorMinimumAmperage, - limitationEnabled: - chargingStation.stationInfo.customValueLimitationMeterValues, - } - ), - currentPerPhaseSampledValueTemplates.L3.fluctuationPercent ?? - Constants.DEFAULT_FLUCTUATION_PERCENT - ) - : undefined - currentMeasurandValues.L1 = - phase1FluctuatedValue ?? - defaultFluctuatedAmperagePerPhase ?? - getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage) - currentMeasurandValues.L2 = - phase2FluctuatedValue ?? - defaultFluctuatedAmperagePerPhase ?? - getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage) - currentMeasurandValues.L3 = - phase3FluctuatedValue ?? - defaultFluctuatedAmperagePerPhase ?? - getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage) - } else { - currentMeasurandValues.L1 = isNotEmptyString(currentSampledValueTemplate.value) - ? getRandomFloatFluctuatedRounded( - getLimitFromSampledValueTemplateCustomValue( - currentSampledValueTemplate.value, - connectorMaximumAmperage, - connectorMinimumAmperage, - { - fallbackValue: connectorMinimumAmperage, - limitationEnabled: - chargingStation.stationInfo.customValueLimitationMeterValues, - } - ), - currentSampledValueTemplate.fluctuationPercent ?? - Constants.DEFAULT_FLUCTUATION_PERCENT - ) - : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage) - currentMeasurandValues.L2 = 0 - currentMeasurandValues.L3 = 0 - } - currentMeasurandValues.allPhases = roundTo( - (currentMeasurandValues.L1 + currentMeasurandValues.L2 + currentMeasurandValues.L3) / - chargingStation.getNumberOfPhases(), - 2 - ) - break - case CurrentType.DC: - connectorMaximumAmperage = DCElectricUtils.amperage( + : DCElectricUtils.amperage( connectorMaximumAvailablePower, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion chargingStation.stationInfo.voltageOut! ) - currentMeasurandValues.allPhases = isNotEmptyString(currentSampledValueTemplate.value) - ? getRandomFloatFluctuatedRounded( - getLimitFromSampledValueTemplateCustomValue( - currentSampledValueTemplate.value, - connectorMaximumAmperage, - connectorMinimumAmperage, - { - fallbackValue: connectorMinimumAmperage, - limitationEnabled: - chargingStation.stationInfo.customValueLimitationMeterValues, - } - ), - currentSampledValueTemplate.fluctuationPercent ?? - Constants.DEFAULT_FLUCTUATION_PERCENT - ) - : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage) - break - default: - logger.error(`${chargingStation.logPrefix()} ${errMsg}`) - throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, RequestCommand.METER_VALUES) - } + const connectorMinimumAmperage = currentMeasurand.template.minimumValue ?? 0 + meterValue.sampledValue.push( buildSampledValue( chargingStation.stationInfo.ocppVersion, - currentSampledValueTemplate, - currentMeasurandValues.allPhases + currentMeasurand.template, + currentMeasurand.values.allPhases ) ) const sampledValuesIndex = meterValue.sampledValue.length - 1 - if ( - convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > - connectorMaximumAmperage || - convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) < - connectorMinimumAmperage || + validateCurrentMeasurandValue( + chargingStation, + connectorId, + connector, + meterValue.sampledValue[sampledValuesIndex], + connectorMaximumAmperage, + connectorMinimumAmperage, 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: ${connectorMinimumAmperage.toString()}/${meterValue.sampledValue[ - sampledValuesIndex - ].value.toString()}/${connectorMaximumAmperage.toString()}` - ) - } + ) for ( let phase = 1; chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases(); phase++ ) { - const phaseValue = `L${phase.toString()}` + const phaseValue = `L${phase.toString()}` as MeterValuePhase meterValue.sampledValue.push( buildSampledValue( chargingStation.stationInfo.ocppVersion, - currentPerPhaseSampledValueTemplates[ + currentMeasurand.perPhaseTemplates[ phaseValue as keyof MeasurandPerPhaseSampledValueTemplates - ] ?? currentSampledValueTemplate, - currentMeasurandValues[phaseValue as keyof MeasurandPerPhaseSampledValueTemplates], + ] ?? currentMeasurand.template, + currentMeasurand.values[phaseValue as keyof MeasurandPerPhaseSampledValueTemplates], undefined, - phaseValue as MeterValuePhase + phaseValue ) ) const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1 - if ( - convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) > - connectorMaximumAmperage || - convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) < - connectorMinimumAmperage || + validateCurrentMeasurandPhaseValue( + chargingStation, + connectorId, + connector, + meterValue.sampledValue[sampledValuesPerPhaseIndex], + connectorMaximumAmperage, + connectorMinimumAmperage, debug - ) { - logger.error( - `${chargingStation.logPrefix()} MeterValues measurand ${ - meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ?? - MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER - }: phase ${ - // 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.toString()}/${connectorMaximumAmperage.toString()}` - ) - } + ) } } // Energy.Active.Import.Register measurand (default) - energySampledValueTemplate = getSampledValueTemplate(chargingStation, connectorId) - if (energySampledValueTemplate != null) { - checkMeasurandPowerDivider(chargingStation, energySampledValueTemplate.measurand) + const energyMeasurand = buildEnergyMeasurandValue(chargingStation, connectorId, interval) + if (energyMeasurand != null) { + updateConnectorEnergyValues(connector, energyMeasurand.value) const unitDivider = - energySampledValueTemplate.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1 - connectorMaximumAvailablePower == null && - (connectorMaximumAvailablePower = - chargingStation.getConnectorMaximumAvailablePower(connectorId)) + energyMeasurand.template.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1 + const energySampledValue = buildSampledValue( + chargingStation.stationInfo.ocppVersion, + energyMeasurand.template, + roundTo( + chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) / + unitDivider, + 2 + ) + ) + meterValue.sampledValue.push(energySampledValue) + const 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 || + const connectorMinimumEnergyRounded = roundTo(energyMeasurand.template.minimumValue ?? 0, 2) + validateEnergyMeasurandValue( + chargingStation, + connectorId, + energySampledValue, + energyMeasurand.value, + connectorMinimumEnergyRounded, + connectorMaximumEnergyRounded, + interval, 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 + } case OCPPVersion.VERSION_20: case OCPPVersion.VERSION_201: { const meterValue: OCPP20MeterValue = { @@ -1115,231 +1308,101 @@ export const buildMeterValue = ( 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 socMeasurand = buildSocMeasurandValue(chargingStation, connectorId) + if (socMeasurand != null) { + const socSampledValue = buildSampledValue( + chargingStation.stationInfo.ocppVersion, + socMeasurand.template, + socMeasurand.value ) - const sampledValuesIndex = meterValue.sampledValue.length - 1 - if ( - convertToInt(meterValue.sampledValue[sampledValuesIndex].value) > socMaximumValue || - convertToInt(meterValue.sampledValue[sampledValuesIndex].value) < socMinimumValue || + meterValue.sampledValue.push(socSampledValue) + validateSocMeasurandValue( + chargingStation, + connectorId, + socSampledValue, + socMeasurand.template.minimumValue ?? 0, + 100, 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 - ) - ) - } + const voltageMeasurand = buildVoltageMeasurandValue(chargingStation, connectorId) + if (voltageMeasurand != null) { + addMainVoltageToMeterValue(chargingStation, meterValue, voltageMeasurand) for ( let phase = 1; chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases(); phase++ ) { - const phaseLineToNeutralValue = `L${phase.toString()}-N` - const voltagePhaseLineToNeutralSampledValueTemplate = getSampledValueTemplate( + addPhaseVoltageToMeterValue( chargingStation, connectorId, - MeterValueMeasurand.VOLTAGE, - phaseLineToNeutralValue as MeterValuePhase + meterValue, + voltageMeasurand, + phase ) - 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 - ) + addLineToLineVoltageToMeterValue( + chargingStation, + connectorId, + meterValue, + voltageMeasurand, + phase ) - 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 energyMeasurand = buildEnergyMeasurandValue(chargingStation, connectorId, interval) + if (energyMeasurand != null) { + updateConnectorEnergyValues(connector, energyMeasurand.value) const unitDivider = - energySampledValueTemplate.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1 - connectorMaximumAvailablePower == null && - (connectorMaximumAvailablePower = - chargingStation.getConnectorMaximumAvailablePower(connectorId)) + energyMeasurand.template.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1 + const energySampledValue = buildSampledValue( + chargingStation.stationInfo.ocppVersion, + energyMeasurand.template, + roundTo( + chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) / + unitDivider, + 2 + ) + ) + meterValue.sampledValue.push(energySampledValue) + const connectorMaximumAvailablePower = + chargingStation.getConnectorMaximumAvailablePower(connectorId) const connectorMaximumEnergyRounded = roundTo( (connectorMaximumAvailablePower * interval) / (3600 * 1000), 2 ) - const connectorMinimumEnergyRounded = roundTo( - energySampledValueTemplate.minimumValue ?? 0, - 2 + const connectorMinimumEnergyRounded = roundTo(energyMeasurand.template.minimumValue ?? 0, 2) + validateEnergyMeasurandValue( + chargingStation, + connectorId, + energySampledValue, + energyMeasurand.value, + connectorMinimumEnergyRounded, + connectorMaximumEnergyRounded, + interval, + debug ) - 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 - ) - ) + } + // Power.Active.Import measurand + const powerMeasurand = buildPowerMeasurandValue(chargingStation, connectorId) + if (powerMeasurand?.values.allPhases != null) { + const powerSampledValue = buildSampledValue( + chargingStation.stationInfo.ocppVersion, + powerMeasurand.template, + powerMeasurand.values.allPhases ) - 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` - ) - } + meterValue.sampledValue.push(powerSampledValue) + } + // Current.Import measurand + const currentMeasurand = buildCurrentMeasurandValue(chargingStation, connectorId) + if (currentMeasurand?.values.allPhases != null) { + const currentSampledValue = buildSampledValue( + chargingStation.stationInfo.ocppVersion, + currentMeasurand.template, + currentMeasurand.values.allPhases + ) + meterValue.sampledValue.push(currentSampledValue) } return meterValue } @@ -1563,23 +1626,23 @@ function buildSampledValue ( context?: MeterValueContext, phase?: MeterValuePhase ): SampledValue { - const sampledValueContext = context ?? sampledValueTemplate.context + const sampledValueMeasurand = sampledValueTemplate.measurand ?? getMeasurandDefault() + const sampledValueUnit = + sampledValueTemplate.unit ?? getMeasurandDefaultUnit(sampledValueMeasurand) + const sampledValueContext = + context ?? sampledValueTemplate.context ?? getMeasurandDefaultContext(sampledValueMeasurand) const sampledValueLocation = - sampledValueTemplate.location ?? getMeasurandDefaultLocation(sampledValueTemplate.measurand) + sampledValueTemplate.location ?? getMeasurandDefaultLocation(sampledValueMeasurand) const sampledValuePhase = phase ?? sampledValueTemplate.phase 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 }), + context: sampledValueContext, + location: sampledValueLocation, + measurand: sampledValueMeasurand, + unit: sampledValueUnit, value: value.toString(), // OCPP 1.6 uses string ...(sampledValuePhase != null && { phase: sampledValuePhase }), } as OCPP16SampledValue @@ -1587,16 +1650,12 @@ function buildSampledValue ( case OCPPVersion.VERSION_201: // OCPP 2.0 format return { - ...(sampledValueContext != null && { context: sampledValueContext }), - ...(sampledValueTemplate.measurand != null && { - measurand: sampledValueTemplate.measurand, - }), - ...(sampledValueLocation != null && { location: sampledValueLocation }), + context: sampledValueContext, + location: sampledValueLocation, + measurand: sampledValueMeasurand, + ...(sampledValueUnit !== undefined && { unitOfMeasure: { unit: sampledValueUnit } }), value, // OCPP 2.0 uses number ...(sampledValuePhase != null && { phase: sampledValuePhase }), - ...(sampledValueTemplate.unitOfMeasure != null && { - unitOfMeasure: sampledValueTemplate.unitOfMeasure, - }), } as OCPP20SampledValue default: throw new OCPPError( @@ -1608,36 +1667,123 @@ function buildSampledValue ( } } +const getMeasurandDefaultContext = (measurandType: MeterValueMeasurand): MeterValueContext => { + return MeterValueContext.SAMPLE_PERIODIC +} + const getMeasurandDefaultLocation = ( measurandType: MeterValueMeasurand ): MeterValueLocation | undefined => { switch (measurandType) { + case MeterValueMeasurand.CURRENT_EXPORT: + case MeterValueMeasurand.CURRENT_IMPORT: + case MeterValueMeasurand.CURRENT_OFFERED: + return MeterValueLocation.OUTLET + + case MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_INTERVAL: + case MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER: + case MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL: + case MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER: + case MeterValueMeasurand.ENERGY_ACTIVE_NET: + case MeterValueMeasurand.ENERGY_APPARENT_EXPORT: + case MeterValueMeasurand.ENERGY_APPARENT_IMPORT: + case MeterValueMeasurand.ENERGY_APPARENT_NET: + case MeterValueMeasurand.ENERGY_REACTIVE_EXPORT_INTERVAL: + case MeterValueMeasurand.ENERGY_REACTIVE_EXPORT_REGISTER: + case MeterValueMeasurand.ENERGY_REACTIVE_IMPORT_INTERVAL: + case MeterValueMeasurand.ENERGY_REACTIVE_IMPORT_REGISTER: + case MeterValueMeasurand.ENERGY_REACTIVE_NET: + return MeterValueLocation.OUTLET + + case MeterValueMeasurand.FAN_RPM: + return MeterValueLocation.BODY + + case MeterValueMeasurand.FREQUENCY: + return MeterValueLocation.OUTLET + + case MeterValueMeasurand.POWER_ACTIVE_EXPORT: + case MeterValueMeasurand.POWER_ACTIVE_IMPORT: + case MeterValueMeasurand.POWER_FACTOR: + case MeterValueMeasurand.POWER_OFFERED: + case MeterValueMeasurand.POWER_REACTIVE_EXPORT: + case MeterValueMeasurand.POWER_REACTIVE_IMPORT: + return MeterValueLocation.OUTLET + case MeterValueMeasurand.STATE_OF_CHARGE: return MeterValueLocation.EV + + case MeterValueMeasurand.TEMPERATURE: + return MeterValueLocation.OUTLET + + case MeterValueMeasurand.VOLTAGE: + return MeterValueLocation.OUTLET + + default: + return undefined } } -// const getMeasurandDefaultUnit = ( -// measurandType: MeterValueMeasurand -// ): MeterValueUnit | undefined => { -// switch (measurandType) { -// case MeterValueMeasurand.CURRENT_EXPORT: -// case MeterValueMeasurand.CURRENT_IMPORT: -// case MeterValueMeasurand.CURRENT_OFFERED: -// return MeterValueUnit.AMP -// case MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER: -// case MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER: -// return MeterValueUnit.WATT_HOUR -// case MeterValueMeasurand.POWER_ACTIVE_EXPORT: -// case MeterValueMeasurand.POWER_ACTIVE_IMPORT: -// case MeterValueMeasurand.POWER_OFFERED: -// return MeterValueUnit.WATT -// case MeterValueMeasurand.STATE_OF_CHARGE: -// return MeterValueUnit.PERCENT -// case MeterValueMeasurand.VOLTAGE: -// return MeterValueUnit.VOLT -// } -// } +const getMeasurandDefault = (): MeterValueMeasurand => { + return MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER +} + +const getMeasurandDefaultUnit = ( + measurandType: MeterValueMeasurand +): MeterValueUnit | undefined => { + switch (measurandType) { + case MeterValueMeasurand.CURRENT_EXPORT: + case MeterValueMeasurand.CURRENT_IMPORT: + case MeterValueMeasurand.CURRENT_OFFERED: + return MeterValueUnit.AMP + + case MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_INTERVAL: + case MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER: + case MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL: + case MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER: + case MeterValueMeasurand.ENERGY_ACTIVE_NET: + case MeterValueMeasurand.ENERGY_APPARENT_EXPORT: + case MeterValueMeasurand.ENERGY_APPARENT_IMPORT: + case MeterValueMeasurand.ENERGY_APPARENT_NET: + return MeterValueUnit.WATT_HOUR + + case MeterValueMeasurand.ENERGY_REACTIVE_EXPORT_INTERVAL: + case MeterValueMeasurand.ENERGY_REACTIVE_EXPORT_REGISTER: + case MeterValueMeasurand.ENERGY_REACTIVE_IMPORT_INTERVAL: + case MeterValueMeasurand.ENERGY_REACTIVE_IMPORT_REGISTER: + case MeterValueMeasurand.ENERGY_REACTIVE_NET: + return MeterValueUnit.VAR_HOUR + + case MeterValueMeasurand.FAN_RPM: + return MeterValueUnit.REVOLUTIONS_PER_MINUTE + + case MeterValueMeasurand.FREQUENCY: + return MeterValueUnit.HERTZ + + case MeterValueMeasurand.POWER_ACTIVE_EXPORT: + case MeterValueMeasurand.POWER_ACTIVE_IMPORT: + case MeterValueMeasurand.POWER_OFFERED: + return MeterValueUnit.WATT + + case MeterValueMeasurand.POWER_FACTOR: + return undefined + + case MeterValueMeasurand.POWER_REACTIVE_EXPORT: + case MeterValueMeasurand.POWER_REACTIVE_IMPORT: + return MeterValueUnit.VAR + + case MeterValueMeasurand.STATE_OF_CHARGE: + return MeterValueUnit.PERCENT + + case MeterValueMeasurand.TEMPERATURE: + return MeterValueUnit.TEMP_CELSIUS + + case MeterValueMeasurand.VOLTAGE: + return MeterValueUnit.VOLT + + default: + return undefined + } +} // eslint-disable-next-line @typescript-eslint/no-extraneous-class export class OCPPServiceUtils { -- 2.43.0