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