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