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