b2f1c400f3b0217cbc613b815393f67c9ecd0a08
[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 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 (!chargingStation.hasFeatureProfile(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: ${roundTo(
730 interval / (3600 * 1000),
731 4,
732 )}h`,
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 }