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