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