X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2Focpp%2F1.6%2FOCPP16ServiceUtils.ts;h=23381f9ec3dd63e5e47faed0301fc6fecbc01f75;hb=7671fa0be211e944f027ebd83f3a0ad64c2ef2d6;hp=721ae1c24bb78be42f8b3a250c27a82b801d37a7;hpb=1b6498baa159ac6ebd4eda9008cde367640bf3ed;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts index 721ae1c2..23381f9e 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts @@ -1,56 +1,35 @@ -// Partial Copyright Jerome Benoit. 2021. All Rights Reserved. +// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved. -import { ACElectricUtils, DCElectricUtils } from '../../../utils/ElectricUtils'; -import { CurrentType, Voltage } from '../../../types/ChargingStationTemplate'; -import MeasurandPerPhaseSampledValueTemplates, { - SampledValueTemplate, -} from '../../../types/MeasurandPerPhaseSampledValueTemplates'; +import type { JSONSchemaType } from 'ajv'; + +import type { ChargingStation } from '../../../charging-station'; +import { OCPPError } from '../../../exception'; import { + CurrentType, + ErrorType, + type JsonType, + type MeasurandPerPhaseSampledValueTemplates, + type MeasurandValues, MeterValueContext, MeterValueLocation, MeterValueUnit, - OCPP16MeterValue, + type OCPP16ChargingProfile, + type OCPP16IncomingRequestCommand, + type OCPP16MeterValue, OCPP16MeterValueMeasurand, OCPP16MeterValuePhase, - OCPP16SampledValue, -} from '../../../types/ocpp/1.6/MeterValues'; -import { - OCPP16IncomingRequestCommand, OCPP16RequestCommand, -} from '../../../types/ocpp/1.6/Requests'; -import { + type OCPP16SampledValue, OCPP16StandardParametersKey, - OCPP16SupportedFeatureProfiles, -} from '../../../types/ocpp/1.6/Configuration'; - -import type ChargingStation from '../../ChargingStation'; -import Constants from '../../../utils/Constants'; -import { ErrorType } from '../../../types/ocpp/ErrorType'; -import MeasurandValues from '../../../types/MeasurandValues'; -import OCPPError from '../../../exception/OCPPError'; -import Utils from '../../../utils/Utils'; -import logger from '../../../utils/Logger'; - -export class OCPP16ServiceUtils { - public static checkMeasurandPowerDivider( - chargingStation: ChargingStation, - measurandType: OCPP16MeterValueMeasurand - ): void { - if (Utils.isUndefined(chargingStation.stationInfo.powerDivider)) { - const errMsg = `${chargingStation.logPrefix()} MeterValues measurand ${ - measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER - }: powerDivider is undefined`; - logger.error(errMsg); - throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES); - } else if (chargingStation.stationInfo?.powerDivider <= 0) { - const errMsg = `${chargingStation.logPrefix()} MeterValues measurand ${ - measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER - }: powerDivider have zero or below value ${chargingStation.stationInfo.powerDivider}`; - logger.error(errMsg); - throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES); - } - } + type OCPP16SupportedFeatureProfiles, + OCPPVersion, + type SampledValueTemplate, + Voltage, +} from '../../../types'; +import { ACElectricUtils, Constants, DCElectricUtils, Utils, logger } from '../../../utils'; +import { OCPPServiceUtils } from '../OCPPServiceUtils'; +export class OCPP16ServiceUtils extends OCPPServiceUtils { public static checkFeatureProfile( chargingStation: ChargingStation, featureProfile: OCPP16SupportedFeatureProfiles, @@ -67,63 +46,6 @@ export class OCPP16ServiceUtils { return true; } - public static buildSampledValue( - sampledValueTemplate: SampledValueTemplate, - value: number, - context?: MeterValueContext, - phase?: OCPP16MeterValuePhase - ): OCPP16SampledValue { - const sampledValueValue = value ?? sampledValueTemplate?.value ?? null; - const sampledValueContext = context ?? sampledValueTemplate?.context ?? null; - const sampledValueLocation = - sampledValueTemplate?.location ?? - OCPP16ServiceUtils.getMeasurandDefaultLocation(sampledValueTemplate?.measurand ?? null); - const sampledValuePhase = phase ?? sampledValueTemplate?.phase ?? null; - return { - ...(!Utils.isNullOrUndefined(sampledValueTemplate.unit) && { - unit: sampledValueTemplate.unit, - }), - ...(!Utils.isNullOrUndefined(sampledValueContext) && { context: sampledValueContext }), - ...(!Utils.isNullOrUndefined(sampledValueTemplate.measurand) && { - measurand: sampledValueTemplate.measurand, - }), - ...(!Utils.isNullOrUndefined(sampledValueLocation) && { location: sampledValueLocation }), - ...(!Utils.isNullOrUndefined(sampledValueValue) && { value: sampledValueValue.toString() }), - ...(!Utils.isNullOrUndefined(sampledValuePhase) && { phase: sampledValuePhase }), - }; - } - - public static getMeasurandDefaultUnit( - measurandType: OCPP16MeterValueMeasurand - ): MeterValueUnit | undefined { - switch (measurandType) { - case OCPP16MeterValueMeasurand.CURRENT_EXPORT: - case OCPP16MeterValueMeasurand.CURRENT_IMPORT: - case OCPP16MeterValueMeasurand.CURRENT_OFFERED: - return MeterValueUnit.AMP; - case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER: - case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER: - return MeterValueUnit.WATT_HOUR; - case OCPP16MeterValueMeasurand.POWER_ACTIVE_EXPORT: - case OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT: - case OCPP16MeterValueMeasurand.POWER_OFFERED: - return MeterValueUnit.WATT; - case OCPP16MeterValueMeasurand.STATE_OF_CHARGE: - return MeterValueUnit.PERCENT; - case OCPP16MeterValueMeasurand.VOLTAGE: - return MeterValueUnit.VOLT; - } - } - - public static getMeasurandDefaultLocation( - measurandType: OCPP16MeterValueMeasurand - ): MeterValueLocation | undefined { - switch (measurandType) { - case OCPP16MeterValueMeasurand.STATE_OF_CHARGE: - return MeterValueLocation.EV; - } - } - public static buildMeterValue( chargingStation: ChargingStation, connectorId: number, @@ -132,39 +54,49 @@ export class OCPP16ServiceUtils { debug = false ): OCPP16MeterValue { const meterValue: OCPP16MeterValue = { - timestamp: new Date().toISOString(), + timestamp: new Date(), sampledValue: [], }; const connector = chargingStation.getConnectorStatus(connectorId); // SoC measurand - const socSampledValueTemplate = chargingStation.getSampledValueTemplate( + const socSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate( + chargingStation, connectorId, OCPP16MeterValueMeasurand.STATE_OF_CHARGE ); if (socSampledValueTemplate) { + const socMaximumValue = 100; + const socMinimumValue = socSampledValueTemplate.minimumValue ?? 0; const socSampledValueTemplateValue = socSampledValueTemplate.value ? Utils.getRandomFloatFluctuatedRounded( parseInt(socSampledValueTemplate.value), socSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) - : Utils.getRandomInteger(100); + : Utils.getRandomInteger(socMaximumValue, socMinimumValue); meterValue.sampledValue.push( OCPP16ServiceUtils.buildSampledValue(socSampledValueTemplate, socSampledValueTemplateValue) ); const sampledValuesIndex = meterValue.sampledValue.length - 1; - if (Utils.convertToInt(meterValue.sampledValue[sampledValuesIndex].value) > 100 || debug) { + if ( + Utils.convertToInt(meterValue.sampledValue[sampledValuesIndex].value) > socMaximumValue || + Utils.convertToInt(meterValue.sampledValue[sampledValuesIndex].value) < socMinimumValue || + debug + ) { logger.error( `${chargingStation.logPrefix()} MeterValues measurand ${ meterValue.sampledValue[sampledValuesIndex].measurand ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER - }: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${ + }: connector id ${connectorId}, transaction id ${ + connector?.transactionId + }, value: ${socMinimumValue}/${ meterValue.sampledValue[sampledValuesIndex].value - }/100` + }/${socMaximumValue}}` ); } } // Voltage measurand - const voltageSampledValueTemplate = chargingStation.getSampledValueTemplate( + const voltageSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate( + chargingStation, connectorId, OCPP16MeterValueMeasurand.VOLTAGE ); @@ -193,7 +125,8 @@ export class OCPP16ServiceUtils { ) { const phaseLineToNeutralValue = `L${phase}-N`; const voltagePhaseLineToNeutralSampledValueTemplate = - chargingStation.getSampledValueTemplate( + OCPP16ServiceUtils.getSampledValueTemplate( + chargingStation, connectorId, OCPP16MeterValueMeasurand.VOLTAGE, phaseLineToNeutralValue as OCPP16MeterValuePhase @@ -216,7 +149,7 @@ export class OCPP16ServiceUtils { OCPP16ServiceUtils.buildSampledValue( voltagePhaseLineToNeutralSampledValueTemplate ?? voltageSampledValueTemplate, voltagePhaseLineToNeutralMeasurandValue ?? voltageMeasurandValue, - null, + undefined, phaseLineToNeutralValue as OCPP16MeterValuePhase ) ); @@ -227,7 +160,8 @@ export class OCPP16ServiceUtils { : chargingStation.getNumberOfPhases() }`; const voltagePhaseLineToLineSampledValueTemplate = - chargingStation.getSampledValueTemplate( + OCPP16ServiceUtils.getSampledValueTemplate( + chargingStation, connectorId, OCPP16MeterValueMeasurand.VOLTAGE, phaseLineToLineValue as OCPP16MeterValuePhase @@ -254,7 +188,7 @@ export class OCPP16ServiceUtils { OCPP16ServiceUtils.buildSampledValue( voltagePhaseLineToLineSampledValueTemplate ?? voltageSampledValueTemplate, voltagePhaseLineToLineMeasurandValue ?? defaultVoltagePhaseLineToLineMeasurandValue, - null, + undefined, phaseLineToLineValue as OCPP16MeterValuePhase ) ); @@ -262,24 +196,28 @@ export class OCPP16ServiceUtils { } } // Power.Active.Import measurand - const powerSampledValueTemplate = chargingStation.getSampledValueTemplate( + const powerSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate( + chargingStation, connectorId, OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT ); let powerPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {}; if (chargingStation.getNumberOfPhases() === 3) { powerPerPhaseSampledValueTemplates = { - L1: chargingStation.getSampledValueTemplate( + L1: OCPP16ServiceUtils.getSampledValueTemplate( + chargingStation, connectorId, OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT, OCPP16MeterValuePhase.L1_N ), - L2: chargingStation.getSampledValueTemplate( + L2: OCPP16ServiceUtils.getSampledValueTemplate( + chargingStation, connectorId, OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT, OCPP16MeterValuePhase.L2_N ), - L3: chargingStation.getSampledValueTemplate( + L3: OCPP16ServiceUtils.getSampledValueTemplate( + chargingStation, connectorId, OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT, OCPP16MeterValuePhase.L3_N @@ -291,7 +229,7 @@ export class OCPP16ServiceUtils { chargingStation, powerSampledValueTemplate.measurand ); - const errMsg = `${chargingStation.logPrefix()} MeterValues measurand ${ + const errMsg = `MeterValues measurand ${ powerSampledValueTemplate.measurand ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER }: Unknown ${chargingStation.getCurrentOutType()} currentOutType in template file ${ @@ -308,57 +246,93 @@ export class OCPP16ServiceUtils { const connectorMaximumPowerPerPhase = Math.round( connectorMaximumAvailablePower / chargingStation.getNumberOfPhases() ); + const connectorMinimumPower = Math.round(powerSampledValueTemplate.minimumValue) ?? 0; + const connectorMinimumPowerPerPhase = Math.round( + connectorMinimumPower / chargingStation.getNumberOfPhases() + ); switch (chargingStation.getCurrentOutType()) { case CurrentType.AC: if (chargingStation.getNumberOfPhases() === 3) { const defaultFluctuatedPowerPerPhase = powerSampledValueTemplate.value && Utils.getRandomFloatFluctuatedRounded( - parseInt(powerSampledValueTemplate.value) / chargingStation.getNumberOfPhases(), + OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( + powerSampledValueTemplate.value, + connectorMaximumPower / unitDivider, + { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() } + ) / chargingStation.getNumberOfPhases(), powerSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ); const phase1FluctuatedValue = powerPerPhaseSampledValueTemplates?.L1?.value && Utils.getRandomFloatFluctuatedRounded( - parseInt(powerPerPhaseSampledValueTemplates.L1.value), + OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( + powerPerPhaseSampledValueTemplates.L1.value, + connectorMaximumPowerPerPhase / unitDivider, + { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() } + ), powerPerPhaseSampledValueTemplates.L1.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ); const phase2FluctuatedValue = powerPerPhaseSampledValueTemplates?.L2?.value && Utils.getRandomFloatFluctuatedRounded( - parseInt(powerPerPhaseSampledValueTemplates.L2.value), + OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( + powerPerPhaseSampledValueTemplates.L2.value, + connectorMaximumPowerPerPhase / unitDivider, + { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() } + ), powerPerPhaseSampledValueTemplates.L2.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ); const phase3FluctuatedValue = powerPerPhaseSampledValueTemplates?.L3?.value && Utils.getRandomFloatFluctuatedRounded( - parseInt(powerPerPhaseSampledValueTemplates.L3.value), + OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( + powerPerPhaseSampledValueTemplates.L3.value, + connectorMaximumPowerPerPhase / unitDivider, + { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() } + ), powerPerPhaseSampledValueTemplates.L3.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ); powerMeasurandValues.L1 = phase1FluctuatedValue ?? defaultFluctuatedPowerPerPhase ?? - Utils.getRandomFloatRounded(connectorMaximumPowerPerPhase / unitDivider); + Utils.getRandomFloatRounded( + connectorMaximumPowerPerPhase / unitDivider, + connectorMinimumPowerPerPhase / unitDivider + ); powerMeasurandValues.L2 = phase2FluctuatedValue ?? defaultFluctuatedPowerPerPhase ?? - Utils.getRandomFloatRounded(connectorMaximumPowerPerPhase / unitDivider); + Utils.getRandomFloatRounded( + connectorMaximumPowerPerPhase / unitDivider, + connectorMinimumPowerPerPhase / unitDivider + ); powerMeasurandValues.L3 = phase3FluctuatedValue ?? defaultFluctuatedPowerPerPhase ?? - Utils.getRandomFloatRounded(connectorMaximumPowerPerPhase / unitDivider); + Utils.getRandomFloatRounded( + connectorMaximumPowerPerPhase / unitDivider, + connectorMinimumPowerPerPhase / unitDivider + ); } else { powerMeasurandValues.L1 = powerSampledValueTemplate.value ? Utils.getRandomFloatFluctuatedRounded( - parseInt(powerSampledValueTemplate.value), + OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( + powerSampledValueTemplate.value, + connectorMaximumPower / unitDivider, + { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() } + ), powerSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) - : Utils.getRandomFloatRounded(connectorMaximumPower / unitDivider); + : Utils.getRandomFloatRounded( + connectorMaximumPower / unitDivider, + connectorMinimumPower / unitDivider + ); powerMeasurandValues.L2 = 0; powerMeasurandValues.L3 = 0; } @@ -370,14 +344,21 @@ export class OCPP16ServiceUtils { case CurrentType.DC: powerMeasurandValues.allPhases = powerSampledValueTemplate.value ? Utils.getRandomFloatFluctuatedRounded( - parseInt(powerSampledValueTemplate.value), + OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( + powerSampledValueTemplate.value, + connectorMaximumPower / unitDivider, + { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() } + ), powerSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) - : Utils.getRandomFloatRounded(connectorMaximumPower / unitDivider); + : Utils.getRandomFloatRounded( + connectorMaximumPower / unitDivider, + connectorMinimumPower / unitDivider + ); break; default: - logger.error(errMsg); + logger.error(`${chargingStation.logPrefix()} ${errMsg}`); throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES); } meterValue.sampledValue.push( @@ -388,16 +369,21 @@ export class OCPP16ServiceUtils { ); const sampledValuesIndex = meterValue.sampledValue.length - 1; const connectorMaximumPowerRounded = Utils.roundTo(connectorMaximumPower / unitDivider, 2); + const connectorMinimumPowerRounded = Utils.roundTo(connectorMinimumPower / unitDivider, 2); if ( Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > connectorMaximumPowerRounded || + Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) < + connectorMinimumPowerRounded || debug ) { logger.error( `${chargingStation.logPrefix()} MeterValues measurand ${ meterValue.sampledValue[sampledValuesIndex].measurand ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER - }: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${ + }: connector id ${connectorId}, transaction id ${ + connector?.transactionId + }, value: ${connectorMinimumPowerRounded}/${ meterValue.sampledValue[sampledValuesIndex].value }/${connectorMaximumPowerRounded}` ); @@ -413,7 +399,7 @@ export class OCPP16ServiceUtils { (powerPerPhaseSampledValueTemplates[`L${phase}`] as SampledValueTemplate) ?? powerSampledValueTemplate, powerMeasurandValues[`L${phase}`] as number, - null, + undefined, phaseValue as OCPP16MeterValuePhase ) ); @@ -422,9 +408,15 @@ export class OCPP16ServiceUtils { connectorMaximumPowerPerPhase / unitDivider, 2 ); + const connectorMinimumPowerPerPhaseRounded = Utils.roundTo( + connectorMinimumPowerPerPhase / unitDivider, + 2 + ); if ( Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) > connectorMaximumPowerPerPhaseRounded || + Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) < + connectorMinimumPowerPerPhaseRounded || debug ) { logger.error( @@ -433,7 +425,9 @@ export class OCPP16ServiceUtils { OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER }: phase ${ meterValue.sampledValue[sampledValuesPerPhaseIndex].phase - }, connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${ + }, connector id ${connectorId}, transaction id ${ + connector?.transactionId + }, value: ${connectorMinimumPowerPerPhaseRounded}/${ meterValue.sampledValue[sampledValuesPerPhaseIndex].value }/${connectorMaximumPowerPerPhaseRounded}` ); @@ -441,24 +435,28 @@ export class OCPP16ServiceUtils { } } // Current.Import measurand - const currentSampledValueTemplate = chargingStation.getSampledValueTemplate( + const currentSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate( + chargingStation, connectorId, OCPP16MeterValueMeasurand.CURRENT_IMPORT ); let currentPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {}; if (chargingStation.getNumberOfPhases() === 3) { currentPerPhaseSampledValueTemplates = { - L1: chargingStation.getSampledValueTemplate( + L1: OCPP16ServiceUtils.getSampledValueTemplate( + chargingStation, connectorId, OCPP16MeterValueMeasurand.CURRENT_IMPORT, OCPP16MeterValuePhase.L1 ), - L2: chargingStation.getSampledValueTemplate( + L2: OCPP16ServiceUtils.getSampledValueTemplate( + chargingStation, connectorId, OCPP16MeterValueMeasurand.CURRENT_IMPORT, OCPP16MeterValuePhase.L2 ), - L3: chargingStation.getSampledValueTemplate( + L3: OCPP16ServiceUtils.getSampledValueTemplate( + chargingStation, connectorId, OCPP16MeterValueMeasurand.CURRENT_IMPORT, OCPP16MeterValuePhase.L3 @@ -470,7 +468,7 @@ export class OCPP16ServiceUtils { chargingStation, currentSampledValueTemplate.measurand ); - const errMsg = `${chargingStation.logPrefix()} MeterValues measurand ${ + const errMsg = `MeterValues measurand ${ currentSampledValueTemplate.measurand ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER }: Unknown ${chargingStation.getCurrentOutType()} currentOutType in template file ${ @@ -482,6 +480,7 @@ export class OCPP16ServiceUtils { const currentMeasurandValues: MeasurandValues = {} as MeasurandValues; const connectorMaximumAvailablePower = chargingStation.getConnectorMaximumAvailablePower(connectorId); + const connectorMinimumAmperage = currentSampledValueTemplate.minimumValue ?? 0; let connectorMaximumAmperage: number; switch (chargingStation.getCurrentOutType()) { case CurrentType.AC: @@ -494,51 +493,71 @@ export class OCPP16ServiceUtils { const defaultFluctuatedAmperagePerPhase = currentSampledValueTemplate.value && Utils.getRandomFloatFluctuatedRounded( - parseInt(currentSampledValueTemplate.value), + OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( + currentSampledValueTemplate.value, + connectorMaximumAmperage, + { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() } + ), currentSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ); const phase1FluctuatedValue = currentPerPhaseSampledValueTemplates?.L1?.value && Utils.getRandomFloatFluctuatedRounded( - parseInt(currentPerPhaseSampledValueTemplates.L1.value), + OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( + currentPerPhaseSampledValueTemplates.L1.value, + connectorMaximumAmperage, + { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() } + ), currentPerPhaseSampledValueTemplates.L1.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ); const phase2FluctuatedValue = currentPerPhaseSampledValueTemplates?.L2?.value && Utils.getRandomFloatFluctuatedRounded( - parseInt(currentPerPhaseSampledValueTemplates.L2.value), + OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( + currentPerPhaseSampledValueTemplates.L2.value, + connectorMaximumAmperage, + { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() } + ), currentPerPhaseSampledValueTemplates.L2.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ); const phase3FluctuatedValue = currentPerPhaseSampledValueTemplates?.L3?.value && Utils.getRandomFloatFluctuatedRounded( - parseInt(currentPerPhaseSampledValueTemplates.L3.value), + OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( + currentPerPhaseSampledValueTemplates.L3.value, + connectorMaximumAmperage, + { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() } + ), currentPerPhaseSampledValueTemplates.L3.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ); currentMeasurandValues.L1 = phase1FluctuatedValue ?? defaultFluctuatedAmperagePerPhase ?? - Utils.getRandomFloatRounded(connectorMaximumAmperage); + Utils.getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage); currentMeasurandValues.L2 = phase2FluctuatedValue ?? defaultFluctuatedAmperagePerPhase ?? - Utils.getRandomFloatRounded(connectorMaximumAmperage); + Utils.getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage); currentMeasurandValues.L3 = phase3FluctuatedValue ?? defaultFluctuatedAmperagePerPhase ?? - Utils.getRandomFloatRounded(connectorMaximumAmperage); + Utils.getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage); } else { currentMeasurandValues.L1 = currentSampledValueTemplate.value ? Utils.getRandomFloatFluctuatedRounded( - parseInt(currentSampledValueTemplate.value), + OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( + currentSampledValueTemplate.value, + connectorMaximumAmperage, + { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() } + ), currentSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) - : Utils.getRandomFloatRounded(connectorMaximumAmperage); + : Utils.getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage); currentMeasurandValues.L2 = 0; currentMeasurandValues.L3 = 0; } @@ -555,14 +574,18 @@ export class OCPP16ServiceUtils { ); currentMeasurandValues.allPhases = currentSampledValueTemplate.value ? Utils.getRandomFloatFluctuatedRounded( - parseInt(currentSampledValueTemplate.value), + OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( + currentSampledValueTemplate.value, + connectorMaximumAmperage, + { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() } + ), currentSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) - : Utils.getRandomFloatRounded(connectorMaximumAmperage); + : Utils.getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage); break; default: - logger.error(errMsg); + logger.error(`${chargingStation.logPrefix()} ${errMsg}`); throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES); } meterValue.sampledValue.push( @@ -575,13 +598,17 @@ export class OCPP16ServiceUtils { if ( Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > connectorMaximumAmperage || + Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) < + connectorMinimumAmperage || debug ) { logger.error( `${chargingStation.logPrefix()} MeterValues measurand ${ meterValue.sampledValue[sampledValuesIndex].measurand ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER - }: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${ + }: connector id ${connectorId}, transaction id ${ + connector?.transactionId + }, value: ${connectorMinimumAmperage}/${ meterValue.sampledValue[sampledValuesIndex].value }/${connectorMaximumAmperage}` ); @@ -597,7 +624,7 @@ export class OCPP16ServiceUtils { (currentPerPhaseSampledValueTemplates[phaseValue] as SampledValueTemplate) ?? currentSampledValueTemplate, currentMeasurandValues[phaseValue] as number, - null, + undefined, phaseValue as OCPP16MeterValuePhase ) ); @@ -605,6 +632,8 @@ export class OCPP16ServiceUtils { if ( Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) > connectorMaximumAmperage || + Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) < + connectorMinimumAmperage || debug ) { logger.error( @@ -613,7 +642,9 @@ export class OCPP16ServiceUtils { OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER }: phase ${ meterValue.sampledValue[sampledValuesPerPhaseIndex].phase - }, connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${ + }, connector id ${connectorId}, transaction id ${ + connector?.transactionId + }, value: ${connectorMinimumAmperage}/${ meterValue.sampledValue[sampledValuesPerPhaseIndex].value }/${connectorMaximumAmperage}` ); @@ -621,7 +652,10 @@ export class OCPP16ServiceUtils { } } // Energy.Active.Import.Register measurand (default) - const energySampledValueTemplate = chargingStation.getSampledValueTemplate(connectorId); + const energySampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate( + chargingStation, + connectorId + ); if (energySampledValueTemplate) { OCPP16ServiceUtils.checkMeasurandPowerDivider( chargingStation, @@ -638,16 +672,23 @@ export class OCPP16ServiceUtils { const energyValueRounded = energySampledValueTemplate.value ? // Cumulate the fluctuated value around the static one Utils.getRandomFloatFluctuatedRounded( - parseInt(energySampledValueTemplate.value), + OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue( + energySampledValueTemplate.value, + connectorMaximumEnergyRounded, + { + limitationEnabled: chargingStation.getCustomValueLimitationMeterValues(), + unitMultiplier: unitDivider, + } + ), energySampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) : Utils.getRandomFloatRounded(connectorMaximumEnergyRounded); // Persist previous value on connector if ( connector && - !Utils.isNullOrUndefined(connector.energyActiveImportRegisterValue) && + Utils.isNullOrUndefined(connector.energyActiveImportRegisterValue) === false && connector.energyActiveImportRegisterValue >= 0 && - !Utils.isNullOrUndefined(connector.transactionEnergyActiveImportRegisterValue) && + Utils.isNullOrUndefined(connector.transactionEnergyActiveImportRegisterValue) === false && connector.transactionEnergyActiveImportRegisterValue >= 0 ) { connector.energyActiveImportRegisterValue += energyValueRounded; @@ -672,8 +713,8 @@ export class OCPP16ServiceUtils { `${chargingStation.logPrefix()} MeterValues measurand ${ meterValue.sampledValue[sampledValuesIndex].measurand ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER - }: connectorId ${connectorId}, transaction ${ - connector.transactionId + }: connector id ${connectorId}, transaction id ${ + connector?.transactionId }, value: ${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${Utils.roundTo( interval / (3600 * 1000), 4 @@ -690,16 +731,19 @@ export class OCPP16ServiceUtils { meterStart: number ): OCPP16MeterValue { const meterValue: OCPP16MeterValue = { - timestamp: new Date().toISOString(), + timestamp: new Date(), sampledValue: [], }; // Energy.Active.Import.Register measurand (default) - const sampledValueTemplate = chargingStation.getSampledValueTemplate(connectorId); + const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate( + chargingStation, + connectorId + ); const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1; meterValue.sampledValue.push( OCPP16ServiceUtils.buildSampledValue( sampledValueTemplate, - Utils.roundTo(meterStart / unitDivider, 4), + Utils.roundTo((meterStart ?? 0) / unitDivider, 4), MeterValueContext.TRANSACTION_BEGIN ) ); @@ -712,16 +756,19 @@ export class OCPP16ServiceUtils { meterStop: number ): OCPP16MeterValue { const meterValue: OCPP16MeterValue = { - timestamp: new Date().toISOString(), + timestamp: new Date(), sampledValue: [], }; // Energy.Active.Import.Register measurand (default) - const sampledValueTemplate = chargingStation.getSampledValueTemplate(connectorId); + const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate( + chargingStation, + connectorId + ); const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1; meterValue.sampledValue.push( OCPP16ServiceUtils.buildSampledValue( sampledValueTemplate, - Utils.roundTo(meterStop / unitDivider, 4), + Utils.roundTo((meterStop ?? 0) / unitDivider, 4), MeterValueContext.TRANSACTION_END ) ); @@ -737,4 +784,132 @@ export class OCPP16ServiceUtils { meterValues.push(transactionEndMeterValue); return meterValues; } + + public static setChargingProfile( + chargingStation: ChargingStation, + connectorId: number, + cp: OCPP16ChargingProfile + ): void { + if ( + Utils.isNullOrUndefined(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles) + ) { + logger.error( + `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an uninitialized charging profiles array attribute, applying deferred initialization` + ); + chargingStation.getConnectorStatus(connectorId).chargingProfiles = []; + } + if ( + Array.isArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles) === false + ) { + logger.error( + `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an improper attribute type for the charging profiles array, applying proper type initialization` + ); + chargingStation.getConnectorStatus(connectorId).chargingProfiles = []; + } + let cpReplaced = false; + if (Utils.isNotEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) { + chargingStation + .getConnectorStatus(connectorId) + ?.chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => { + if ( + chargingProfile.chargingProfileId === cp.chargingProfileId || + (chargingProfile.stackLevel === cp.stackLevel && + chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose) + ) { + chargingStation.getConnectorStatus(connectorId).chargingProfiles[index] = cp; + cpReplaced = true; + } + }); + } + !cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp); + } + + public static parseJsonSchemaFile( + relativePath: string, + moduleName?: string, + methodName?: string + ): JSONSchemaType { + return super.parseJsonSchemaFile( + relativePath, + OCPPVersion.VERSION_16, + moduleName, + methodName + ); + } + + private static buildSampledValue( + sampledValueTemplate: SampledValueTemplate, + value: number, + context?: MeterValueContext, + phase?: OCPP16MeterValuePhase + ): OCPP16SampledValue { + const sampledValueValue = value ?? sampledValueTemplate?.value ?? null; + const sampledValueContext = context ?? sampledValueTemplate?.context ?? null; + const sampledValueLocation = + sampledValueTemplate?.location ?? + OCPP16ServiceUtils.getMeasurandDefaultLocation(sampledValueTemplate?.measurand ?? null); + const sampledValuePhase = phase ?? sampledValueTemplate?.phase ?? null; + return { + ...(!Utils.isNullOrUndefined(sampledValueTemplate.unit) && { + unit: sampledValueTemplate.unit, + }), + ...(!Utils.isNullOrUndefined(sampledValueContext) && { context: sampledValueContext }), + ...(!Utils.isNullOrUndefined(sampledValueTemplate.measurand) && { + measurand: sampledValueTemplate.measurand, + }), + ...(!Utils.isNullOrUndefined(sampledValueLocation) && { location: sampledValueLocation }), + ...(!Utils.isNullOrUndefined(sampledValueValue) && { value: sampledValueValue.toString() }), + ...(!Utils.isNullOrUndefined(sampledValuePhase) && { phase: sampledValuePhase }), + }; + } + + private static checkMeasurandPowerDivider( + chargingStation: ChargingStation, + measurandType: OCPP16MeterValueMeasurand + ): void { + if (Utils.isUndefined(chargingStation.powerDivider)) { + const errMsg = `MeterValues measurand ${ + measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER + }: powerDivider is undefined`; + logger.error(`${chargingStation.logPrefix()} ${errMsg}`); + throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES); + } else if (chargingStation?.powerDivider <= 0) { + const errMsg = `MeterValues measurand ${ + measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER + }: powerDivider have zero or below value ${chargingStation.powerDivider}`; + logger.error(`${chargingStation.logPrefix()} ${errMsg}`); + throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES); + } + } + + private static getMeasurandDefaultLocation( + measurandType: OCPP16MeterValueMeasurand + ): MeterValueLocation | undefined { + switch (measurandType) { + case OCPP16MeterValueMeasurand.STATE_OF_CHARGE: + return MeterValueLocation.EV; + } + } + + private static getMeasurandDefaultUnit( + measurandType: OCPP16MeterValueMeasurand + ): MeterValueUnit | undefined { + switch (measurandType) { + case OCPP16MeterValueMeasurand.CURRENT_EXPORT: + case OCPP16MeterValueMeasurand.CURRENT_IMPORT: + case OCPP16MeterValueMeasurand.CURRENT_OFFERED: + return MeterValueUnit.AMP; + case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER: + case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER: + return MeterValueUnit.WATT_HOUR; + case OCPP16MeterValueMeasurand.POWER_ACTIVE_EXPORT: + case OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT: + case OCPP16MeterValueMeasurand.POWER_OFFERED: + return MeterValueUnit.WATT; + case OCPP16MeterValueMeasurand.STATE_OF_CHARGE: + return MeterValueUnit.PERCENT; + case OCPP16MeterValueMeasurand.VOLTAGE: + return MeterValueUnit.VOLT; + } + } }