feat: add mininum value support to meterValues generation
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16ServiceUtils.ts
CommitLineData
edd13439 1// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
c8eeb62b 2
130783a7
JB
3import path from 'node:path';
4import { fileURLToPath } from 'node:url';
5
6import type { JSONSchemaType } from 'ajv';
7
2896e06d 8import type { ChargingStation } from '../../../charging-station';
268a74bb 9import { OCPPError } from '../../../exception';
e7aeea18 10import {
268a74bb
JB
11 CurrentType,
12 ErrorType,
13 type JsonType,
14 type MeasurandPerPhaseSampledValueTemplates,
15 type MeasurandValues,
e7aeea18
JB
16 MeterValueContext,
17 MeterValueLocation,
18 MeterValueUnit,
268a74bb
JB
19 type OCPP16ChargingProfile,
20 type OCPP16IncomingRequestCommand,
27782dbc 21 type OCPP16MeterValue,
e7aeea18
JB
22 OCPP16MeterValueMeasurand,
23 OCPP16MeterValuePhase,
370ae4ee 24 OCPP16RequestCommand,
268a74bb
JB
25 type OCPP16SampledValue,
26 OCPP16StandardParametersKey,
27 type OCPP16SupportedFeatureProfiles,
28 OCPPVersion,
29 type SampledValueTemplate,
30 Voltage,
31} from '../../../types';
60a74391 32import { ACElectricUtils, Constants, DCElectricUtils, Utils, logger } from '../../../utils';
2896e06d 33import { OCPPServiceUtils } from '../internal';
6ed92bc1 34
7bc31f9c 35export class OCPP16ServiceUtils extends OCPPServiceUtils {
370ae4ee
JB
36 public static checkFeatureProfile(
37 chargingStation: ChargingStation,
38 featureProfile: OCPP16SupportedFeatureProfiles,
39 command: OCPP16RequestCommand | OCPP16IncomingRequestCommand
40 ): boolean {
41 if (!chargingStation.hasFeatureProfile(featureProfile)) {
42 logger.warn(
43 `${chargingStation.logPrefix()} Trying to '${command}' without '${featureProfile}' feature enabled in ${
44 OCPP16StandardParametersKey.SupportedFeatureProfiles
45 } in configuration`
46 );
47 return false;
48 }
49 return true;
50 }
51
78085c42
JB
52 public static buildMeterValue(
53 chargingStation: ChargingStation,
54 connectorId: number,
55 transactionId: number,
56 interval: number,
57 debug = false
58 ): OCPP16MeterValue {
59 const meterValue: OCPP16MeterValue = {
c38f0ced 60 timestamp: new Date(),
78085c42
JB
61 sampledValue: [],
62 };
63 const connector = chargingStation.getConnectorStatus(connectorId);
64 // SoC measurand
ed3d2808 65 const socSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 66 chargingStation,
78085c42
JB
67 connectorId,
68 OCPP16MeterValueMeasurand.STATE_OF_CHARGE
69 );
70 if (socSampledValueTemplate) {
860ef183
JB
71 const socMaximumValue = 100;
72 const socMinimumValue = socSampledValueTemplate.minimumValue ?? 0;
78085c42
JB
73 const socSampledValueTemplateValue = socSampledValueTemplate.value
74 ? Utils.getRandomFloatFluctuatedRounded(
75 parseInt(socSampledValueTemplate.value),
76 socSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT
77 )
860ef183 78 : Utils.getRandomInteger(socMaximumValue, socMinimumValue);
78085c42
JB
79 meterValue.sampledValue.push(
80 OCPP16ServiceUtils.buildSampledValue(socSampledValueTemplate, socSampledValueTemplateValue)
81 );
82 const sampledValuesIndex = meterValue.sampledValue.length - 1;
860ef183
JB
83 if (
84 Utils.convertToInt(meterValue.sampledValue[sampledValuesIndex].value) > socMaximumValue ||
85 Utils.convertToInt(meterValue.sampledValue[sampledValuesIndex].value) < socMinimumValue ||
86 debug
87 ) {
78085c42
JB
88 logger.error(
89 `${chargingStation.logPrefix()} MeterValues measurand ${
90 meterValue.sampledValue[sampledValuesIndex].measurand ??
91 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
860ef183
JB
92 }: connectorId ${connectorId}, transaction ${
93 connector?.transactionId
94 }, value: ${socMinimumValue}/${
78085c42 95 meterValue.sampledValue[sampledValuesIndex].value
860ef183 96 }/${socMaximumValue}}`
78085c42
JB
97 );
98 }
99 }
100 // Voltage measurand
ed3d2808 101 const voltageSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 102 chargingStation,
78085c42
JB
103 connectorId,
104 OCPP16MeterValueMeasurand.VOLTAGE
105 );
106 if (voltageSampledValueTemplate) {
107 const voltageSampledValueTemplateValue = voltageSampledValueTemplate.value
108 ? parseInt(voltageSampledValueTemplate.value)
109 : chargingStation.getVoltageOut();
110 const fluctuationPercent =
111 voltageSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT;
112 const voltageMeasurandValue = Utils.getRandomFloatFluctuatedRounded(
113 voltageSampledValueTemplateValue,
114 fluctuationPercent
115 );
116 if (
117 chargingStation.getNumberOfPhases() !== 3 ||
118 (chargingStation.getNumberOfPhases() === 3 && chargingStation.getMainVoltageMeterValues())
119 ) {
120 meterValue.sampledValue.push(
121 OCPP16ServiceUtils.buildSampledValue(voltageSampledValueTemplate, voltageMeasurandValue)
122 );
123 }
124 for (
125 let phase = 1;
126 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
127 phase++
128 ) {
129 const phaseLineToNeutralValue = `L${phase}-N`;
130 const voltagePhaseLineToNeutralSampledValueTemplate =
ed3d2808 131 OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 132 chargingStation,
78085c42
JB
133 connectorId,
134 OCPP16MeterValueMeasurand.VOLTAGE,
135 phaseLineToNeutralValue as OCPP16MeterValuePhase
136 );
137 let voltagePhaseLineToNeutralMeasurandValue: number;
138 if (voltagePhaseLineToNeutralSampledValueTemplate) {
139 const voltagePhaseLineToNeutralSampledValueTemplateValue =
140 voltagePhaseLineToNeutralSampledValueTemplate.value
141 ? parseInt(voltagePhaseLineToNeutralSampledValueTemplate.value)
142 : chargingStation.getVoltageOut();
143 const fluctuationPhaseToNeutralPercent =
144 voltagePhaseLineToNeutralSampledValueTemplate.fluctuationPercent ??
145 Constants.DEFAULT_FLUCTUATION_PERCENT;
146 voltagePhaseLineToNeutralMeasurandValue = Utils.getRandomFloatFluctuatedRounded(
147 voltagePhaseLineToNeutralSampledValueTemplateValue,
148 fluctuationPhaseToNeutralPercent
149 );
150 }
151 meterValue.sampledValue.push(
152 OCPP16ServiceUtils.buildSampledValue(
153 voltagePhaseLineToNeutralSampledValueTemplate ?? voltageSampledValueTemplate,
154 voltagePhaseLineToNeutralMeasurandValue ?? voltageMeasurandValue,
72092cfc 155 undefined,
78085c42
JB
156 phaseLineToNeutralValue as OCPP16MeterValuePhase
157 )
158 );
159 if (chargingStation.getPhaseLineToLineVoltageMeterValues()) {
160 const phaseLineToLineValue = `L${phase}-L${
161 (phase + 1) % chargingStation.getNumberOfPhases() !== 0
162 ? (phase + 1) % chargingStation.getNumberOfPhases()
163 : chargingStation.getNumberOfPhases()
164 }`;
165 const voltagePhaseLineToLineSampledValueTemplate =
ed3d2808 166 OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 167 chargingStation,
78085c42
JB
168 connectorId,
169 OCPP16MeterValueMeasurand.VOLTAGE,
170 phaseLineToLineValue as OCPP16MeterValuePhase
171 );
172 let voltagePhaseLineToLineMeasurandValue: number;
173 if (voltagePhaseLineToLineSampledValueTemplate) {
174 const voltagePhaseLineToLineSampledValueTemplateValue =
175 voltagePhaseLineToLineSampledValueTemplate.value
176 ? parseInt(voltagePhaseLineToLineSampledValueTemplate.value)
177 : Voltage.VOLTAGE_400;
178 const fluctuationPhaseLineToLinePercent =
179 voltagePhaseLineToLineSampledValueTemplate.fluctuationPercent ??
180 Constants.DEFAULT_FLUCTUATION_PERCENT;
181 voltagePhaseLineToLineMeasurandValue = Utils.getRandomFloatFluctuatedRounded(
182 voltagePhaseLineToLineSampledValueTemplateValue,
183 fluctuationPhaseLineToLinePercent
184 );
185 }
186 const defaultVoltagePhaseLineToLineMeasurandValue = Utils.getRandomFloatFluctuatedRounded(
187 Voltage.VOLTAGE_400,
188 fluctuationPercent
189 );
190 meterValue.sampledValue.push(
191 OCPP16ServiceUtils.buildSampledValue(
192 voltagePhaseLineToLineSampledValueTemplate ?? voltageSampledValueTemplate,
193 voltagePhaseLineToLineMeasurandValue ?? defaultVoltagePhaseLineToLineMeasurandValue,
72092cfc 194 undefined,
78085c42
JB
195 phaseLineToLineValue as OCPP16MeterValuePhase
196 )
197 );
198 }
199 }
200 }
201 // Power.Active.Import measurand
ed3d2808 202 const powerSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 203 chargingStation,
78085c42
JB
204 connectorId,
205 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT
206 );
abe9e9dd 207 let powerPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {};
78085c42
JB
208 if (chargingStation.getNumberOfPhases() === 3) {
209 powerPerPhaseSampledValueTemplates = {
ed3d2808 210 L1: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 211 chargingStation,
78085c42
JB
212 connectorId,
213 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
214 OCPP16MeterValuePhase.L1_N
215 ),
ed3d2808 216 L2: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 217 chargingStation,
78085c42
JB
218 connectorId,
219 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
220 OCPP16MeterValuePhase.L2_N
221 ),
ed3d2808 222 L3: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 223 chargingStation,
78085c42
JB
224 connectorId,
225 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
226 OCPP16MeterValuePhase.L3_N
227 ),
228 };
229 }
230 if (powerSampledValueTemplate) {
231 OCPP16ServiceUtils.checkMeasurandPowerDivider(
232 chargingStation,
233 powerSampledValueTemplate.measurand
234 );
fc040c43 235 const errMsg = `MeterValues measurand ${
78085c42
JB
236 powerSampledValueTemplate.measurand ??
237 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
238 }: Unknown ${chargingStation.getCurrentOutType()} currentOutType in template file ${
2484ac1e 239 chargingStation.templateFile
78085c42
JB
240 }, cannot calculate ${
241 powerSampledValueTemplate.measurand ??
242 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
243 } measurand value`;
abe9e9dd 244 const powerMeasurandValues = {} as MeasurandValues;
78085c42 245 const unitDivider = powerSampledValueTemplate?.unit === MeterValueUnit.KILO_WATT ? 1000 : 1;
1b6498ba
JB
246 const connectorMaximumAvailablePower =
247 chargingStation.getConnectorMaximumAvailablePower(connectorId);
248 const connectorMaximumPower = Math.round(connectorMaximumAvailablePower);
ad8537a7 249 const connectorMaximumPowerPerPhase = Math.round(
1b6498ba 250 connectorMaximumAvailablePower / chargingStation.getNumberOfPhases()
78085c42 251 );
860ef183
JB
252 const connectorMinimumPower = Math.round(powerSampledValueTemplate.minimumValue) ?? 0;
253 const connectorMinimumPowerPerPhase = Math.round(
254 connectorMinimumPower / chargingStation.getNumberOfPhases()
255 );
78085c42
JB
256 switch (chargingStation.getCurrentOutType()) {
257 case CurrentType.AC:
258 if (chargingStation.getNumberOfPhases() === 3) {
259 const defaultFluctuatedPowerPerPhase =
260 powerSampledValueTemplate.value &&
261 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
262 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
263 powerSampledValueTemplate.value,
264 connectorMaximumPower / unitDivider,
265 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 266 ) / chargingStation.getNumberOfPhases(),
78085c42
JB
267 powerSampledValueTemplate.fluctuationPercent ??
268 Constants.DEFAULT_FLUCTUATION_PERCENT
269 );
270 const phase1FluctuatedValue =
271 powerPerPhaseSampledValueTemplates?.L1?.value &&
272 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
273 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
274 powerPerPhaseSampledValueTemplates.L1.value,
275 connectorMaximumPowerPerPhase / unitDivider,
276 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 277 ),
78085c42
JB
278 powerPerPhaseSampledValueTemplates.L1.fluctuationPercent ??
279 Constants.DEFAULT_FLUCTUATION_PERCENT
280 );
281 const phase2FluctuatedValue =
282 powerPerPhaseSampledValueTemplates?.L2?.value &&
283 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
284 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
285 powerPerPhaseSampledValueTemplates.L2.value,
286 connectorMaximumPowerPerPhase / unitDivider,
287 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 288 ),
78085c42
JB
289 powerPerPhaseSampledValueTemplates.L2.fluctuationPercent ??
290 Constants.DEFAULT_FLUCTUATION_PERCENT
291 );
292 const phase3FluctuatedValue =
293 powerPerPhaseSampledValueTemplates?.L3?.value &&
294 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
295 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
296 powerPerPhaseSampledValueTemplates.L3.value,
297 connectorMaximumPowerPerPhase / unitDivider,
298 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 299 ),
78085c42
JB
300 powerPerPhaseSampledValueTemplates.L3.fluctuationPercent ??
301 Constants.DEFAULT_FLUCTUATION_PERCENT
302 );
303 powerMeasurandValues.L1 =
304 phase1FluctuatedValue ??
305 defaultFluctuatedPowerPerPhase ??
860ef183
JB
306 Utils.getRandomFloatRounded(
307 connectorMaximumPowerPerPhase / unitDivider,
308 connectorMinimumPowerPerPhase / unitDivider
309 );
78085c42
JB
310 powerMeasurandValues.L2 =
311 phase2FluctuatedValue ??
312 defaultFluctuatedPowerPerPhase ??
860ef183
JB
313 Utils.getRandomFloatRounded(
314 connectorMaximumPowerPerPhase / unitDivider,
315 connectorMinimumPowerPerPhase / unitDivider
316 );
78085c42
JB
317 powerMeasurandValues.L3 =
318 phase3FluctuatedValue ??
319 defaultFluctuatedPowerPerPhase ??
860ef183
JB
320 Utils.getRandomFloatRounded(
321 connectorMaximumPowerPerPhase / unitDivider,
322 connectorMinimumPowerPerPhase / unitDivider
323 );
78085c42
JB
324 } else {
325 powerMeasurandValues.L1 = powerSampledValueTemplate.value
326 ? Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
327 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
328 powerSampledValueTemplate.value,
329 connectorMaximumPower / unitDivider,
330 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 331 ),
78085c42
JB
332 powerSampledValueTemplate.fluctuationPercent ??
333 Constants.DEFAULT_FLUCTUATION_PERCENT
334 )
860ef183
JB
335 : Utils.getRandomFloatRounded(
336 connectorMaximumPower / unitDivider,
337 connectorMinimumPower / unitDivider
338 );
78085c42
JB
339 powerMeasurandValues.L2 = 0;
340 powerMeasurandValues.L3 = 0;
341 }
342 powerMeasurandValues.allPhases = Utils.roundTo(
343 powerMeasurandValues.L1 + powerMeasurandValues.L2 + powerMeasurandValues.L3,
344 2
345 );
346 break;
347 case CurrentType.DC:
348 powerMeasurandValues.allPhases = powerSampledValueTemplate.value
349 ? Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
350 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
351 powerSampledValueTemplate.value,
352 connectorMaximumPower / unitDivider,
353 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 354 ),
78085c42
JB
355 powerSampledValueTemplate.fluctuationPercent ??
356 Constants.DEFAULT_FLUCTUATION_PERCENT
357 )
860ef183
JB
358 : Utils.getRandomFloatRounded(
359 connectorMaximumPower / unitDivider,
360 connectorMinimumPower / unitDivider
361 );
78085c42
JB
362 break;
363 default:
fc040c43 364 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
78085c42
JB
365 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
366 }
367 meterValue.sampledValue.push(
368 OCPP16ServiceUtils.buildSampledValue(
369 powerSampledValueTemplate,
370 powerMeasurandValues.allPhases
371 )
372 );
373 const sampledValuesIndex = meterValue.sampledValue.length - 1;
ad8537a7 374 const connectorMaximumPowerRounded = Utils.roundTo(connectorMaximumPower / unitDivider, 2);
860ef183 375 const connectorMinimumPowerRounded = Utils.roundTo(connectorMinimumPower / unitDivider, 2);
78085c42 376 if (
71a77ac2 377 Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
ad8537a7 378 connectorMaximumPowerRounded ||
860ef183
JB
379 Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) <
380 connectorMinimumPowerRounded ||
78085c42
JB
381 debug
382 ) {
383 logger.error(
384 `${chargingStation.logPrefix()} MeterValues measurand ${
385 meterValue.sampledValue[sampledValuesIndex].measurand ??
386 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
860ef183
JB
387 }: connectorId ${connectorId}, transaction ${
388 connector?.transactionId
389 }, value: ${connectorMinimumPowerRounded}/${
78085c42 390 meterValue.sampledValue[sampledValuesIndex].value
ad8537a7 391 }/${connectorMaximumPowerRounded}`
78085c42
JB
392 );
393 }
394 for (
395 let phase = 1;
396 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
397 phase++
398 ) {
399 const phaseValue = `L${phase}-N`;
400 meterValue.sampledValue.push(
401 OCPP16ServiceUtils.buildSampledValue(
402 (powerPerPhaseSampledValueTemplates[`L${phase}`] as SampledValueTemplate) ??
403 powerSampledValueTemplate,
404 powerMeasurandValues[`L${phase}`] as number,
72092cfc 405 undefined,
78085c42
JB
406 phaseValue as OCPP16MeterValuePhase
407 )
408 );
409 const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1;
ad8537a7
JB
410 const connectorMaximumPowerPerPhaseRounded = Utils.roundTo(
411 connectorMaximumPowerPerPhase / unitDivider,
412 2
413 );
860ef183
JB
414 const connectorMinimumPowerPerPhaseRounded = Utils.roundTo(
415 connectorMinimumPowerPerPhase / unitDivider,
416 2
417 );
78085c42
JB
418 if (
419 Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
ad8537a7 420 connectorMaximumPowerPerPhaseRounded ||
860ef183
JB
421 Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
422 connectorMinimumPowerPerPhaseRounded ||
78085c42
JB
423 debug
424 ) {
425 logger.error(
426 `${chargingStation.logPrefix()} MeterValues measurand ${
427 meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ??
428 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
429 }: phase ${
430 meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
860ef183
JB
431 }, connectorId ${connectorId}, transaction ${
432 connector?.transactionId
433 }, value: ${connectorMinimumPowerPerPhaseRounded}/${
78085c42 434 meterValue.sampledValue[sampledValuesPerPhaseIndex].value
ad8537a7 435 }/${connectorMaximumPowerPerPhaseRounded}`
78085c42
JB
436 );
437 }
438 }
439 }
440 // Current.Import measurand
ed3d2808 441 const currentSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 442 chargingStation,
78085c42
JB
443 connectorId,
444 OCPP16MeterValueMeasurand.CURRENT_IMPORT
445 );
abe9e9dd 446 let currentPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {};
78085c42
JB
447 if (chargingStation.getNumberOfPhases() === 3) {
448 currentPerPhaseSampledValueTemplates = {
ed3d2808 449 L1: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 450 chargingStation,
78085c42
JB
451 connectorId,
452 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
453 OCPP16MeterValuePhase.L1
454 ),
ed3d2808 455 L2: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 456 chargingStation,
78085c42
JB
457 connectorId,
458 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
459 OCPP16MeterValuePhase.L2
460 ),
ed3d2808 461 L3: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 462 chargingStation,
78085c42
JB
463 connectorId,
464 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
465 OCPP16MeterValuePhase.L3
466 ),
467 };
468 }
469 if (currentSampledValueTemplate) {
470 OCPP16ServiceUtils.checkMeasurandPowerDivider(
471 chargingStation,
472 currentSampledValueTemplate.measurand
473 );
fc040c43 474 const errMsg = `MeterValues measurand ${
78085c42
JB
475 currentSampledValueTemplate.measurand ??
476 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
477 }: Unknown ${chargingStation.getCurrentOutType()} currentOutType in template file ${
2484ac1e 478 chargingStation.templateFile
78085c42
JB
479 }, cannot calculate ${
480 currentSampledValueTemplate.measurand ??
481 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
482 } measurand value`;
abe9e9dd 483 const currentMeasurandValues: MeasurandValues = {} as MeasurandValues;
1b6498ba
JB
484 const connectorMaximumAvailablePower =
485 chargingStation.getConnectorMaximumAvailablePower(connectorId);
860ef183 486 const connectorMinimumAmperage = currentSampledValueTemplate.minimumValue ?? 0;
ad8537a7 487 let connectorMaximumAmperage: number;
78085c42
JB
488 switch (chargingStation.getCurrentOutType()) {
489 case CurrentType.AC:
ad8537a7 490 connectorMaximumAmperage = ACElectricUtils.amperagePerPhaseFromPower(
78085c42 491 chargingStation.getNumberOfPhases(),
1b6498ba 492 connectorMaximumAvailablePower,
78085c42
JB
493 chargingStation.getVoltageOut()
494 );
495 if (chargingStation.getNumberOfPhases() === 3) {
496 const defaultFluctuatedAmperagePerPhase =
497 currentSampledValueTemplate.value &&
498 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
499 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
500 currentSampledValueTemplate.value,
501 connectorMaximumAmperage,
502 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
503 ),
78085c42
JB
504 currentSampledValueTemplate.fluctuationPercent ??
505 Constants.DEFAULT_FLUCTUATION_PERCENT
506 );
507 const phase1FluctuatedValue =
508 currentPerPhaseSampledValueTemplates?.L1?.value &&
509 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
510 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
511 currentPerPhaseSampledValueTemplates.L1.value,
512 connectorMaximumAmperage,
513 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 514 ),
78085c42
JB
515 currentPerPhaseSampledValueTemplates.L1.fluctuationPercent ??
516 Constants.DEFAULT_FLUCTUATION_PERCENT
517 );
518 const phase2FluctuatedValue =
519 currentPerPhaseSampledValueTemplates?.L2?.value &&
520 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
521 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
522 currentPerPhaseSampledValueTemplates.L2.value,
523 connectorMaximumAmperage,
524 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 525 ),
78085c42
JB
526 currentPerPhaseSampledValueTemplates.L2.fluctuationPercent ??
527 Constants.DEFAULT_FLUCTUATION_PERCENT
528 );
529 const phase3FluctuatedValue =
530 currentPerPhaseSampledValueTemplates?.L3?.value &&
531 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
532 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
533 currentPerPhaseSampledValueTemplates.L3.value,
534 connectorMaximumAmperage,
535 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 536 ),
78085c42
JB
537 currentPerPhaseSampledValueTemplates.L3.fluctuationPercent ??
538 Constants.DEFAULT_FLUCTUATION_PERCENT
539 );
540 currentMeasurandValues.L1 =
541 phase1FluctuatedValue ??
542 defaultFluctuatedAmperagePerPhase ??
860ef183 543 Utils.getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
78085c42
JB
544 currentMeasurandValues.L2 =
545 phase2FluctuatedValue ??
546 defaultFluctuatedAmperagePerPhase ??
860ef183 547 Utils.getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
78085c42
JB
548 currentMeasurandValues.L3 =
549 phase3FluctuatedValue ??
550 defaultFluctuatedAmperagePerPhase ??
860ef183 551 Utils.getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
78085c42
JB
552 } else {
553 currentMeasurandValues.L1 = currentSampledValueTemplate.value
554 ? Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
555 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
556 currentSampledValueTemplate.value,
557 connectorMaximumAmperage,
558 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
559 ),
78085c42
JB
560 currentSampledValueTemplate.fluctuationPercent ??
561 Constants.DEFAULT_FLUCTUATION_PERCENT
562 )
860ef183 563 : Utils.getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
78085c42
JB
564 currentMeasurandValues.L2 = 0;
565 currentMeasurandValues.L3 = 0;
566 }
567 currentMeasurandValues.allPhases = Utils.roundTo(
568 (currentMeasurandValues.L1 + currentMeasurandValues.L2 + currentMeasurandValues.L3) /
569 chargingStation.getNumberOfPhases(),
570 2
571 );
572 break;
573 case CurrentType.DC:
ad8537a7 574 connectorMaximumAmperage = DCElectricUtils.amperage(
1b6498ba 575 connectorMaximumAvailablePower,
78085c42
JB
576 chargingStation.getVoltageOut()
577 );
578 currentMeasurandValues.allPhases = currentSampledValueTemplate.value
579 ? Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
580 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
581 currentSampledValueTemplate.value,
582 connectorMaximumAmperage,
583 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
584 ),
78085c42
JB
585 currentSampledValueTemplate.fluctuationPercent ??
586 Constants.DEFAULT_FLUCTUATION_PERCENT
587 )
860ef183 588 : Utils.getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
78085c42
JB
589 break;
590 default:
fc040c43 591 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
78085c42
JB
592 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
593 }
594 meterValue.sampledValue.push(
595 OCPP16ServiceUtils.buildSampledValue(
596 currentSampledValueTemplate,
597 currentMeasurandValues.allPhases
598 )
599 );
600 const sampledValuesIndex = meterValue.sampledValue.length - 1;
601 if (
ad8537a7
JB
602 Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
603 connectorMaximumAmperage ||
860ef183
JB
604 Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) <
605 connectorMinimumAmperage ||
78085c42
JB
606 debug
607 ) {
608 logger.error(
609 `${chargingStation.logPrefix()} MeterValues measurand ${
610 meterValue.sampledValue[sampledValuesIndex].measurand ??
611 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
860ef183
JB
612 }: connectorId ${connectorId}, transaction ${
613 connector?.transactionId
614 }, value: ${connectorMinimumAmperage}/${
78085c42 615 meterValue.sampledValue[sampledValuesIndex].value
ad8537a7 616 }/${connectorMaximumAmperage}`
78085c42
JB
617 );
618 }
619 for (
620 let phase = 1;
621 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
622 phase++
623 ) {
624 const phaseValue = `L${phase}`;
625 meterValue.sampledValue.push(
626 OCPP16ServiceUtils.buildSampledValue(
627 (currentPerPhaseSampledValueTemplates[phaseValue] as SampledValueTemplate) ??
628 currentSampledValueTemplate,
629 currentMeasurandValues[phaseValue] as number,
72092cfc 630 undefined,
78085c42
JB
631 phaseValue as OCPP16MeterValuePhase
632 )
633 );
634 const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1;
635 if (
636 Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
ad8537a7 637 connectorMaximumAmperage ||
860ef183
JB
638 Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
639 connectorMinimumAmperage ||
78085c42
JB
640 debug
641 ) {
642 logger.error(
643 `${chargingStation.logPrefix()} MeterValues measurand ${
644 meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ??
645 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
646 }: phase ${
647 meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
860ef183
JB
648 }, connectorId ${connectorId}, transaction ${
649 connector?.transactionId
650 }, value: ${connectorMinimumAmperage}/${
78085c42 651 meterValue.sampledValue[sampledValuesPerPhaseIndex].value
ad8537a7 652 }/${connectorMaximumAmperage}`
78085c42
JB
653 );
654 }
655 }
656 }
657 // Energy.Active.Import.Register measurand (default)
ed3d2808 658 const energySampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab
JB
659 chargingStation,
660 connectorId
661 );
78085c42
JB
662 if (energySampledValueTemplate) {
663 OCPP16ServiceUtils.checkMeasurandPowerDivider(
664 chargingStation,
665 energySampledValueTemplate.measurand
666 );
667 const unitDivider =
668 energySampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
1b6498ba
JB
669 const connectorMaximumAvailablePower =
670 chargingStation.getConnectorMaximumAvailablePower(connectorId);
ad8537a7 671 const connectorMaximumEnergyRounded = Utils.roundTo(
1b6498ba 672 (connectorMaximumAvailablePower * interval) / (3600 * 1000),
78085c42
JB
673 2
674 );
675 const energyValueRounded = energySampledValueTemplate.value
676 ? // Cumulate the fluctuated value around the static one
677 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
678 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
679 energySampledValueTemplate.value,
680 connectorMaximumEnergyRounded,
681 {
682 limitationEnabled: chargingStation.getCustomValueLimitationMeterValues(),
683 unitMultiplier: unitDivider,
684 }
34464008 685 ),
78085c42
JB
686 energySampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT
687 )
ad8537a7 688 : Utils.getRandomFloatRounded(connectorMaximumEnergyRounded);
78085c42
JB
689 // Persist previous value on connector
690 if (
691 connector &&
3a13fc92 692 Utils.isNullOrUndefined(connector.energyActiveImportRegisterValue) === false &&
78085c42 693 connector.energyActiveImportRegisterValue >= 0 &&
3a13fc92 694 Utils.isNullOrUndefined(connector.transactionEnergyActiveImportRegisterValue) === false &&
78085c42
JB
695 connector.transactionEnergyActiveImportRegisterValue >= 0
696 ) {
697 connector.energyActiveImportRegisterValue += energyValueRounded;
698 connector.transactionEnergyActiveImportRegisterValue += energyValueRounded;
699 } else {
700 connector.energyActiveImportRegisterValue = 0;
701 connector.transactionEnergyActiveImportRegisterValue = 0;
702 }
703 meterValue.sampledValue.push(
704 OCPP16ServiceUtils.buildSampledValue(
705 energySampledValueTemplate,
706 Utils.roundTo(
707 chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) /
708 unitDivider,
709 2
710 )
711 )
712 );
713 const sampledValuesIndex = meterValue.sampledValue.length - 1;
ad8537a7 714 if (energyValueRounded > connectorMaximumEnergyRounded || debug) {
78085c42
JB
715 logger.error(
716 `${chargingStation.logPrefix()} MeterValues measurand ${
717 meterValue.sampledValue[sampledValuesIndex].measurand ??
718 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
719 }: connectorId ${connectorId}, transaction ${
72092cfc 720 connector?.transactionId
ad8537a7 721 }, value: ${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${Utils.roundTo(
78085c42
JB
722 interval / (3600 * 1000),
723 4
724 )}h`
725 );
726 }
727 }
728 return meterValue;
729 }
730
e7aeea18
JB
731 public static buildTransactionBeginMeterValue(
732 chargingStation: ChargingStation,
733 connectorId: number,
78085c42 734 meterStart: number
e7aeea18 735 ): OCPP16MeterValue {
fd0c36fa 736 const meterValue: OCPP16MeterValue = {
c38f0ced 737 timestamp: new Date(),
fd0c36fa
JB
738 sampledValue: [],
739 };
9ccca265 740 // Energy.Active.Import.Register measurand (default)
ed3d2808 741 const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab
JB
742 chargingStation,
743 connectorId
744 );
9ccca265 745 const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
e7aeea18
JB
746 meterValue.sampledValue.push(
747 OCPP16ServiceUtils.buildSampledValue(
748 sampledValueTemplate,
2040a613 749 Utils.roundTo((meterStart ?? 0) / unitDivider, 4),
e7aeea18
JB
750 MeterValueContext.TRANSACTION_BEGIN
751 )
752 );
fd0c36fa
JB
753 return meterValue;
754 }
755
e7aeea18
JB
756 public static buildTransactionEndMeterValue(
757 chargingStation: ChargingStation,
758 connectorId: number,
78085c42 759 meterStop: number
e7aeea18 760 ): OCPP16MeterValue {
fd0c36fa 761 const meterValue: OCPP16MeterValue = {
c38f0ced 762 timestamp: new Date(),
fd0c36fa
JB
763 sampledValue: [],
764 };
9ccca265 765 // Energy.Active.Import.Register measurand (default)
ed3d2808 766 const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab
JB
767 chargingStation,
768 connectorId
769 );
9ccca265 770 const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
e7aeea18
JB
771 meterValue.sampledValue.push(
772 OCPP16ServiceUtils.buildSampledValue(
773 sampledValueTemplate,
2040a613 774 Utils.roundTo((meterStop ?? 0) / unitDivider, 4),
e7aeea18
JB
775 MeterValueContext.TRANSACTION_END
776 )
777 );
fd0c36fa
JB
778 return meterValue;
779 }
780
e7aeea18
JB
781 public static buildTransactionDataMeterValues(
782 transactionBeginMeterValue: OCPP16MeterValue,
783 transactionEndMeterValue: OCPP16MeterValue
784 ): OCPP16MeterValue[] {
fd0c36fa
JB
785 const meterValues: OCPP16MeterValue[] = [];
786 meterValues.push(transactionBeginMeterValue);
787 meterValues.push(transactionEndMeterValue);
788 return meterValues;
789 }
7bc31f9c 790
ed3d2808
JB
791 public static setChargingProfile(
792 chargingStation: ChargingStation,
793 connectorId: number,
884a6fdf 794 cp: OCPP16ChargingProfile
ed3d2808 795 ): void {
72092cfc
JB
796 if (
797 Utils.isNullOrUndefined(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)
798 ) {
ed3d2808
JB
799 logger.error(
800 `${chargingStation.logPrefix()} Trying to set a charging profile on connectorId ${connectorId} with an uninitialized charging profiles array attribute, applying deferred initialization`
801 );
802 chargingStation.getConnectorStatus(connectorId).chargingProfiles = [];
803 }
72092cfc
JB
804 if (
805 Array.isArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles) === false
806 ) {
ed3d2808
JB
807 logger.error(
808 `${chargingStation.logPrefix()} Trying to set a charging profile on connectorId ${connectorId} with an improper attribute type for the charging profiles array, applying proper type initialization`
809 );
810 chargingStation.getConnectorStatus(connectorId).chargingProfiles = [];
811 }
812 let cpReplaced = false;
53ac516c 813 if (Utils.isNotEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
ed3d2808
JB
814 chargingStation
815 .getConnectorStatus(connectorId)
72092cfc 816 ?.chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
ed3d2808
JB
817 if (
818 chargingProfile.chargingProfileId === cp.chargingProfileId ||
819 (chargingProfile.stackLevel === cp.stackLevel &&
820 chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)
821 ) {
822 chargingStation.getConnectorStatus(connectorId).chargingProfiles[index] = cp;
823 cpReplaced = true;
824 }
825 });
826 }
72092cfc 827 !cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp);
ed3d2808
JB
828 }
829
1b271a54
JB
830 public static parseJsonSchemaFile<T extends JsonType>(
831 relativePath: string,
832 moduleName?: string,
833 methodName?: string
834 ): JSONSchemaType<T> {
7164966d
JB
835 return super.parseJsonSchemaFile<T>(
836 path.resolve(path.dirname(fileURLToPath(import.meta.url)), relativePath),
1b271a54
JB
837 OCPPVersion.VERSION_16,
838 moduleName,
839 methodName
7164966d 840 );
130783a7
JB
841 }
842
7bc31f9c
JB
843 private static buildSampledValue(
844 sampledValueTemplate: SampledValueTemplate,
845 value: number,
846 context?: MeterValueContext,
847 phase?: OCPP16MeterValuePhase
848 ): OCPP16SampledValue {
849 const sampledValueValue = value ?? sampledValueTemplate?.value ?? null;
850 const sampledValueContext = context ?? sampledValueTemplate?.context ?? null;
851 const sampledValueLocation =
852 sampledValueTemplate?.location ??
853 OCPP16ServiceUtils.getMeasurandDefaultLocation(sampledValueTemplate?.measurand ?? null);
854 const sampledValuePhase = phase ?? sampledValueTemplate?.phase ?? null;
855 return {
856 ...(!Utils.isNullOrUndefined(sampledValueTemplate.unit) && {
857 unit: sampledValueTemplate.unit,
858 }),
859 ...(!Utils.isNullOrUndefined(sampledValueContext) && { context: sampledValueContext }),
860 ...(!Utils.isNullOrUndefined(sampledValueTemplate.measurand) && {
861 measurand: sampledValueTemplate.measurand,
862 }),
863 ...(!Utils.isNullOrUndefined(sampledValueLocation) && { location: sampledValueLocation }),
864 ...(!Utils.isNullOrUndefined(sampledValueValue) && { value: sampledValueValue.toString() }),
865 ...(!Utils.isNullOrUndefined(sampledValuePhase) && { phase: sampledValuePhase }),
866 };
867 }
868
869 private static checkMeasurandPowerDivider(
870 chargingStation: ChargingStation,
871 measurandType: OCPP16MeterValueMeasurand
872 ): void {
fa7bccf4 873 if (Utils.isUndefined(chargingStation.powerDivider)) {
fc040c43 874 const errMsg = `MeterValues measurand ${
7bc31f9c
JB
875 measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
876 }: powerDivider is undefined`;
fc040c43 877 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
7bc31f9c 878 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
fa7bccf4 879 } else if (chargingStation?.powerDivider <= 0) {
fc040c43 880 const errMsg = `MeterValues measurand ${
7bc31f9c 881 measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
fa7bccf4 882 }: powerDivider have zero or below value ${chargingStation.powerDivider}`;
fc040c43 883 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
7bc31f9c
JB
884 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
885 }
886 }
887
888 private static getMeasurandDefaultLocation(
889 measurandType: OCPP16MeterValueMeasurand
890 ): MeterValueLocation | undefined {
891 switch (measurandType) {
892 case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
893 return MeterValueLocation.EV;
894 }
895 }
896
897 private static getMeasurandDefaultUnit(
898 measurandType: OCPP16MeterValueMeasurand
899 ): MeterValueUnit | undefined {
900 switch (measurandType) {
901 case OCPP16MeterValueMeasurand.CURRENT_EXPORT:
902 case OCPP16MeterValueMeasurand.CURRENT_IMPORT:
903 case OCPP16MeterValueMeasurand.CURRENT_OFFERED:
904 return MeterValueUnit.AMP;
905 case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER:
906 case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER:
907 return MeterValueUnit.WATT_HOUR;
908 case OCPP16MeterValueMeasurand.POWER_ACTIVE_EXPORT:
909 case OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT:
910 case OCPP16MeterValueMeasurand.POWER_OFFERED:
911 return MeterValueUnit.WATT;
912 case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
913 return MeterValueUnit.PERCENT;
914 case OCPP16MeterValueMeasurand.VOLTAGE:
915 return MeterValueUnit.VOLT;
916 }
917 }
6ed92bc1 918}