+ if (powerSampledValueTemplate) {
+ OCPP16ServiceUtils.checkMeasurandPowerDivider(
+ chargingStation,
+ powerSampledValueTemplate.measurand!,
+ );
+ const errMsg = `MeterValues measurand ${
+ powerSampledValueTemplate.measurand ??
+ OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
+ }: Unknown ${chargingStation.stationInfo?.currentOutType} currentOutType in template file ${
+ chargingStation.templateFile
+ }, cannot calculate ${
+ powerSampledValueTemplate.measurand ??
+ OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
+ } measurand value`;
+ const powerMeasurandValues: MeasurandValues = {} as MeasurandValues;
+ const unitDivider = powerSampledValueTemplate?.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 =
+ powerSampledValueTemplate.value &&
+ getRandomFloatFluctuatedRounded(
+ OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+ powerSampledValueTemplate.value,
+ connectorMaximumPower / unitDivider,
+ {
+ limitationEnabled:
+ chargingStation.stationInfo?.customValueLimitationMeterValues,
+ },
+ ) / chargingStation.getNumberOfPhases(),
+ powerSampledValueTemplate.fluctuationPercent ??
+ Constants.DEFAULT_FLUCTUATION_PERCENT,
+ );
+ const phase1FluctuatedValue =
+ powerPerPhaseSampledValueTemplates.L1?.value &&
+ getRandomFloatFluctuatedRounded(
+ OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+ powerPerPhaseSampledValueTemplates.L1.value,
+ connectorMaximumPowerPerPhase / unitDivider,
+ {
+ limitationEnabled:
+ chargingStation.stationInfo?.customValueLimitationMeterValues,
+ },
+ ),
+ powerPerPhaseSampledValueTemplates.L1.fluctuationPercent ??
+ Constants.DEFAULT_FLUCTUATION_PERCENT,
+ );
+ const phase2FluctuatedValue =
+ powerPerPhaseSampledValueTemplates.L2?.value &&
+ getRandomFloatFluctuatedRounded(
+ OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+ powerPerPhaseSampledValueTemplates.L2.value,
+ connectorMaximumPowerPerPhase / unitDivider,
+ {
+ limitationEnabled:
+ chargingStation.stationInfo?.customValueLimitationMeterValues,
+ },
+ ),
+ powerPerPhaseSampledValueTemplates.L2.fluctuationPercent ??
+ Constants.DEFAULT_FLUCTUATION_PERCENT,
+ );
+ const phase3FluctuatedValue =
+ powerPerPhaseSampledValueTemplates.L3?.value &&
+ getRandomFloatFluctuatedRounded(
+ OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+ powerPerPhaseSampledValueTemplates.L3.value,
+ connectorMaximumPowerPerPhase / unitDivider,
+ {
+ limitationEnabled:
+ chargingStation.stationInfo?.customValueLimitationMeterValues,
+ },
+ ),
+ powerPerPhaseSampledValueTemplates.L3.fluctuationPercent ??
+ Constants.DEFAULT_FLUCTUATION_PERCENT,
+ );
+ powerMeasurandValues.L1 =
+ (phase1FluctuatedValue as number) ??
+ (defaultFluctuatedPowerPerPhase as number) ??
+ getRandomFloatRounded(
+ connectorMaximumPowerPerPhase / unitDivider,
+ connectorMinimumPowerPerPhase / unitDivider,
+ );
+ powerMeasurandValues.L2 =
+ (phase2FluctuatedValue as number) ??
+ (defaultFluctuatedPowerPerPhase as number) ??
+ getRandomFloatRounded(
+ connectorMaximumPowerPerPhase / unitDivider,
+ connectorMinimumPowerPerPhase / unitDivider,
+ );
+ powerMeasurandValues.L3 =
+ (phase3FluctuatedValue as number) ??
+ (defaultFluctuatedPowerPerPhase as number) ??
+ getRandomFloatRounded(
+ connectorMaximumPowerPerPhase / unitDivider,
+ connectorMinimumPowerPerPhase / unitDivider,
+ );
+ } else {
+ powerMeasurandValues.L1 = powerSampledValueTemplate.value
+ ? getRandomFloatFluctuatedRounded(
+ OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+ powerSampledValueTemplate.value,
+ connectorMaximumPower / 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 = powerSampledValueTemplate.value
+ ? getRandomFloatFluctuatedRounded(
+ OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+ powerSampledValueTemplate.value,
+ connectorMaximumPower / 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, OCPP16RequestCommand.METER_VALUES);
+ }
+ meterValue.sampledValue.push(
+ OCPP16ServiceUtils.buildSampledValue(
+ powerSampledValueTemplate,
+ powerMeasurandValues.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 ||
+ debug
+ ) {
+ logger.error(
+ `${chargingStation.logPrefix()} MeterValues measurand ${
+ meterValue.sampledValue[sampledValuesIndex].measurand ??
+ OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
+ }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumPowerRounded}/${
+ meterValue.sampledValue[sampledValuesIndex].value
+ }/${connectorMaximumPowerRounded}`,
+ );
+ }
+ for (
+ let phase = 1;
+ chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
+ phase++
+ ) {
+ const phaseValue = `L${phase}-N`;
+ meterValue.sampledValue.push(
+ OCPP16ServiceUtils.buildSampledValue(
+ powerPerPhaseSampledValueTemplates[
+ `L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates
+ ]! ?? powerSampledValueTemplate,
+ powerMeasurandValues[`L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates],
+ undefined,
+ phaseValue as OCPP16MeterValuePhase,
+ ),
+ );
+ const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1;
+ const connectorMaximumPowerPerPhaseRounded = roundTo(
+ connectorMaximumPowerPerPhase / unitDivider,
+ 2,
+ );
+ const connectorMinimumPowerPerPhaseRounded = roundTo(
+ connectorMinimumPowerPerPhase / unitDivider,
+ 2,
+ );
+ if (
+ convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
+ connectorMaximumPowerPerPhaseRounded ||
+ convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
+ connectorMinimumPowerPerPhaseRounded ||
+ debug
+ ) {
+ logger.error(
+ `${chargingStation.logPrefix()} MeterValues measurand ${
+ meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ??
+ OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
+ }: phase ${
+ meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
+ }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumPowerPerPhaseRounded}/${
+ meterValue.sampledValue[sampledValuesPerPhaseIndex].value
+ }/${connectorMaximumPowerPerPhaseRounded}`,
+ );
+ }
+ }
+ }
+ // Current.Import measurand
+ const currentSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
+ chargingStation,
+ connectorId,
+ OCPP16MeterValueMeasurand.CURRENT_IMPORT,
+ );
+ let currentPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {};
+ if (chargingStation.getNumberOfPhases() === 3) {
+ currentPerPhaseSampledValueTemplates = {
+ L1: OCPP16ServiceUtils.getSampledValueTemplate(
+ chargingStation,
+ connectorId,
+ OCPP16MeterValueMeasurand.CURRENT_IMPORT,
+ OCPP16MeterValuePhase.L1,
+ ),
+ L2: OCPP16ServiceUtils.getSampledValueTemplate(
+ chargingStation,
+ connectorId,
+ OCPP16MeterValueMeasurand.CURRENT_IMPORT,
+ OCPP16MeterValuePhase.L2,
+ ),
+ L3: OCPP16ServiceUtils.getSampledValueTemplate(
+ chargingStation,
+ connectorId,
+ OCPP16MeterValueMeasurand.CURRENT_IMPORT,
+ OCPP16MeterValuePhase.L3,
+ ),
+ };
+ }
+ if (currentSampledValueTemplate) {
+ OCPP16ServiceUtils.checkMeasurandPowerDivider(
+ chargingStation,
+ currentSampledValueTemplate.measurand!,
+ );
+ const errMsg = `MeterValues measurand ${
+ currentSampledValueTemplate.measurand ??
+ OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
+ }: Unknown ${chargingStation.stationInfo?.currentOutType} currentOutType in template file ${
+ chargingStation.templateFile
+ }, cannot calculate ${
+ currentSampledValueTemplate.measurand ??
+ OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
+ } measurand value`;
+ const currentMeasurandValues: MeasurandValues = {} as MeasurandValues;
+ const connectorMaximumAvailablePower =
+ chargingStation.getConnectorMaximumAvailablePower(connectorId);
+ const connectorMinimumAmperage = currentSampledValueTemplate.minimumValue ?? 0;
+ let connectorMaximumAmperage: number;
+ switch (chargingStation.stationInfo?.currentOutType) {
+ case CurrentType.AC:
+ connectorMaximumAmperage = ACElectricUtils.amperagePerPhaseFromPower(
+ chargingStation.getNumberOfPhases(),
+ connectorMaximumAvailablePower,
+ chargingStation.stationInfo.voltageOut!,
+ );
+ if (chargingStation.getNumberOfPhases() === 3) {
+ const defaultFluctuatedAmperagePerPhase =
+ currentSampledValueTemplate.value &&
+ getRandomFloatFluctuatedRounded(
+ OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+ currentSampledValueTemplate.value,
+ connectorMaximumAmperage,
+ {
+ limitationEnabled:
+ chargingStation.stationInfo?.customValueLimitationMeterValues,
+ },
+ ),
+ currentSampledValueTemplate.fluctuationPercent ??
+ Constants.DEFAULT_FLUCTUATION_PERCENT,
+ );
+ const phase1FluctuatedValue =
+ currentPerPhaseSampledValueTemplates.L1?.value &&
+ getRandomFloatFluctuatedRounded(
+ OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+ currentPerPhaseSampledValueTemplates.L1.value,
+ connectorMaximumAmperage,
+ {
+ limitationEnabled:
+ chargingStation.stationInfo?.customValueLimitationMeterValues,
+ },
+ ),
+ currentPerPhaseSampledValueTemplates.L1.fluctuationPercent ??
+ Constants.DEFAULT_FLUCTUATION_PERCENT,
+ );
+ const phase2FluctuatedValue =
+ currentPerPhaseSampledValueTemplates.L2?.value &&
+ getRandomFloatFluctuatedRounded(
+ OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+ currentPerPhaseSampledValueTemplates.L2.value,
+ connectorMaximumAmperage,
+ {
+ limitationEnabled:
+ chargingStation.stationInfo?.customValueLimitationMeterValues,
+ },
+ ),
+ currentPerPhaseSampledValueTemplates.L2.fluctuationPercent ??
+ Constants.DEFAULT_FLUCTUATION_PERCENT,
+ );
+ const phase3FluctuatedValue =
+ currentPerPhaseSampledValueTemplates.L3?.value &&
+ getRandomFloatFluctuatedRounded(
+ OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+ currentPerPhaseSampledValueTemplates.L3.value,
+ connectorMaximumAmperage,
+ {
+ limitationEnabled:
+ chargingStation.stationInfo?.customValueLimitationMeterValues,
+ },
+ ),
+ currentPerPhaseSampledValueTemplates.L3.fluctuationPercent ??
+ Constants.DEFAULT_FLUCTUATION_PERCENT,
+ );
+ currentMeasurandValues.L1 =
+ (phase1FluctuatedValue as number) ??
+ (defaultFluctuatedAmperagePerPhase as number) ??
+ getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
+ currentMeasurandValues.L2 =
+ (phase2FluctuatedValue as number) ??
+ (defaultFluctuatedAmperagePerPhase as number) ??
+ getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
+ currentMeasurandValues.L3 =
+ (phase3FluctuatedValue as number) ??
+ (defaultFluctuatedAmperagePerPhase as number) ??
+ getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
+ } else {
+ currentMeasurandValues.L1 = currentSampledValueTemplate.value
+ ? getRandomFloatFluctuatedRounded(
+ OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+ currentSampledValueTemplate.value,
+ connectorMaximumAmperage,
+ {
+ 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(
+ connectorMaximumAvailablePower,
+ chargingStation.stationInfo.voltageOut!,
+ );
+ currentMeasurandValues.allPhases = currentSampledValueTemplate.value
+ ? getRandomFloatFluctuatedRounded(
+ OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+ currentSampledValueTemplate.value,
+ connectorMaximumAmperage,
+ {
+ 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, OCPP16RequestCommand.METER_VALUES);
+ }
+ meterValue.sampledValue.push(
+ OCPP16ServiceUtils.buildSampledValue(
+ currentSampledValueTemplate,
+ currentMeasurandValues.allPhases,
+ ),
+ );
+ const sampledValuesIndex = meterValue.sampledValue.length - 1;
+ if (
+ convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
+ connectorMaximumAmperage ||
+ convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) <
+ connectorMinimumAmperage ||
+ debug
+ ) {
+ logger.error(
+ `${chargingStation.logPrefix()} MeterValues measurand ${
+ meterValue.sampledValue[sampledValuesIndex].measurand ??
+ OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
+ }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${
+ meterValue.sampledValue[sampledValuesIndex].value
+ }/${connectorMaximumAmperage}`,
+ );
+ }
+ for (
+ let phase = 1;
+ chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
+ phase++
+ ) {
+ const phaseValue = `L${phase}`;
+ meterValue.sampledValue.push(
+ OCPP16ServiceUtils.buildSampledValue(
+ currentPerPhaseSampledValueTemplates[
+ phaseValue as keyof MeasurandPerPhaseSampledValueTemplates
+ ]! ?? currentSampledValueTemplate,
+ currentMeasurandValues[phaseValue as keyof MeasurandPerPhaseSampledValueTemplates],
+ undefined,
+ phaseValue as OCPP16MeterValuePhase,
+ ),
+ );
+ const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1;
+ if (
+ convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
+ connectorMaximumAmperage ||
+ convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
+ connectorMinimumAmperage ||
+ debug
+ ) {
+ logger.error(
+ `${chargingStation.logPrefix()} MeterValues measurand ${
+ meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ??
+ OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
+ }: phase ${
+ meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
+ }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${
+ meterValue.sampledValue[sampledValuesPerPhaseIndex].value
+ }/${connectorMaximumAmperage}`,
+ );
+ }
+ }
+ }
+ // Energy.Active.Import.Register measurand (default)
+ const energySampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
+ chargingStation,
+ connectorId,
+ );
+ if (energySampledValueTemplate) {
+ OCPP16ServiceUtils.checkMeasurandPowerDivider(
+ chargingStation,
+ energySampledValueTemplate.measurand!,
+ );
+ const unitDivider =
+ energySampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
+ const connectorMaximumAvailablePower =
+ chargingStation.getConnectorMaximumAvailablePower(connectorId);
+ const connectorMaximumEnergyRounded = roundTo(
+ (connectorMaximumAvailablePower * interval) / (3600 * 1000),
+ 2,
+ );
+ const energyValueRounded = energySampledValueTemplate.value
+ ? // Cumulate the fluctuated value around the static one
+ getRandomFloatFluctuatedRounded(
+ OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
+ energySampledValueTemplate.value,
+ connectorMaximumEnergyRounded,
+ {
+ limitationEnabled: chargingStation.stationInfo?.customValueLimitationMeterValues,
+ unitMultiplier: unitDivider,
+ },
+ ),
+ energySampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT,
+ )
+ : getRandomFloatRounded(connectorMaximumEnergyRounded);
+ // Persist previous value on connector
+ if (connector) {
+ if (
+ isNullOrUndefined(connector.energyActiveImportRegisterValue) === false &&
+ connector.energyActiveImportRegisterValue! >= 0 &&
+ isNullOrUndefined(connector.transactionEnergyActiveImportRegisterValue) === false &&
+ connector.transactionEnergyActiveImportRegisterValue! >= 0
+ ) {
+ connector.energyActiveImportRegisterValue! += energyValueRounded;
+ connector.transactionEnergyActiveImportRegisterValue! += energyValueRounded;
+ } else {
+ connector.energyActiveImportRegisterValue = 0;
+ connector.transactionEnergyActiveImportRegisterValue = 0;
+ }
+ }
+ meterValue.sampledValue.push(
+ OCPP16ServiceUtils.buildSampledValue(
+ energySampledValueTemplate,
+ roundTo(
+ chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) /
+ unitDivider,
+ 2,
+ ),
+ ),
+ );
+ const sampledValuesIndex = meterValue.sampledValue.length - 1;
+ if (energyValueRounded > connectorMaximumEnergyRounded || debug) {
+ logger.error(
+ `${chargingStation.logPrefix()} MeterValues measurand ${
+ meterValue.sampledValue[sampledValuesIndex].measurand ??
+ OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
+ }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${interval}ms`,
+ );
+ }
+ }
+ return meterValue;