refactor: cleanup date handling
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16ServiceUtils.ts
1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
2
3 import type { JSONSchemaType } from 'ajv';
4
5 import { type ChargingStation, 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[
415 `L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates
416 ]! ?? powerSampledValueTemplate,
417 powerMeasurandValues[`L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates],
418 undefined,
419 phaseValue as OCPP16MeterValuePhase,
420 ),
421 );
422 const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1;
423 const connectorMaximumPowerPerPhaseRounded = roundTo(
424 connectorMaximumPowerPerPhase / unitDivider,
425 2,
426 );
427 const connectorMinimumPowerPerPhaseRounded = roundTo(
428 connectorMinimumPowerPerPhase / unitDivider,
429 2,
430 );
431 if (
432 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
433 connectorMaximumPowerPerPhaseRounded ||
434 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
435 connectorMinimumPowerPerPhaseRounded ||
436 debug
437 ) {
438 logger.error(
439 `${chargingStation.logPrefix()} MeterValues measurand ${
440 meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ??
441 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
442 }: phase ${
443 meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
444 }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumPowerPerPhaseRounded}/${
445 meterValue.sampledValue[sampledValuesPerPhaseIndex].value
446 }/${connectorMaximumPowerPerPhaseRounded}`,
447 );
448 }
449 }
450 }
451 // Current.Import measurand
452 const currentSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
453 chargingStation,
454 connectorId,
455 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
456 );
457 let currentPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {};
458 if (chargingStation.getNumberOfPhases() === 3) {
459 currentPerPhaseSampledValueTemplates = {
460 L1: OCPP16ServiceUtils.getSampledValueTemplate(
461 chargingStation,
462 connectorId,
463 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
464 OCPP16MeterValuePhase.L1,
465 ),
466 L2: OCPP16ServiceUtils.getSampledValueTemplate(
467 chargingStation,
468 connectorId,
469 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
470 OCPP16MeterValuePhase.L2,
471 ),
472 L3: OCPP16ServiceUtils.getSampledValueTemplate(
473 chargingStation,
474 connectorId,
475 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
476 OCPP16MeterValuePhase.L3,
477 ),
478 };
479 }
480 if (currentSampledValueTemplate) {
481 OCPP16ServiceUtils.checkMeasurandPowerDivider(
482 chargingStation,
483 currentSampledValueTemplate.measurand!,
484 );
485 const errMsg = `MeterValues measurand ${
486 currentSampledValueTemplate.measurand ??
487 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
488 }: Unknown ${chargingStation.getCurrentOutType()} currentOutType in template file ${
489 chargingStation.templateFile
490 }, cannot calculate ${
491 currentSampledValueTemplate.measurand ??
492 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
493 } measurand value`;
494 const currentMeasurandValues: MeasurandValues = {} as MeasurandValues;
495 const connectorMaximumAvailablePower =
496 chargingStation.getConnectorMaximumAvailablePower(connectorId);
497 const connectorMinimumAmperage = currentSampledValueTemplate.minimumValue ?? 0;
498 let connectorMaximumAmperage: number;
499 switch (chargingStation.getCurrentOutType()) {
500 case CurrentType.AC:
501 connectorMaximumAmperage = ACElectricUtils.amperagePerPhaseFromPower(
502 chargingStation.getNumberOfPhases(),
503 connectorMaximumAvailablePower,
504 chargingStation.getVoltageOut(),
505 );
506 if (chargingStation.getNumberOfPhases() === 3) {
507 const defaultFluctuatedAmperagePerPhase =
508 currentSampledValueTemplate.value &&
509 getRandomFloatFluctuatedRounded(
510 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
511 currentSampledValueTemplate.value,
512 connectorMaximumAmperage,
513 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() },
514 ),
515 currentSampledValueTemplate.fluctuationPercent ??
516 Constants.DEFAULT_FLUCTUATION_PERCENT,
517 );
518 const phase1FluctuatedValue =
519 currentPerPhaseSampledValueTemplates.L1?.value &&
520 getRandomFloatFluctuatedRounded(
521 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
522 currentPerPhaseSampledValueTemplates.L1.value,
523 connectorMaximumAmperage,
524 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() },
525 ),
526 currentPerPhaseSampledValueTemplates.L1.fluctuationPercent ??
527 Constants.DEFAULT_FLUCTUATION_PERCENT,
528 );
529 const phase2FluctuatedValue =
530 currentPerPhaseSampledValueTemplates.L2?.value &&
531 getRandomFloatFluctuatedRounded(
532 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
533 currentPerPhaseSampledValueTemplates.L2.value,
534 connectorMaximumAmperage,
535 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() },
536 ),
537 currentPerPhaseSampledValueTemplates.L2.fluctuationPercent ??
538 Constants.DEFAULT_FLUCTUATION_PERCENT,
539 );
540 const phase3FluctuatedValue =
541 currentPerPhaseSampledValueTemplates.L3?.value &&
542 getRandomFloatFluctuatedRounded(
543 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
544 currentPerPhaseSampledValueTemplates.L3.value,
545 connectorMaximumAmperage,
546 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() },
547 ),
548 currentPerPhaseSampledValueTemplates.L3.fluctuationPercent ??
549 Constants.DEFAULT_FLUCTUATION_PERCENT,
550 );
551 currentMeasurandValues.L1 =
552 (phase1FluctuatedValue as number) ??
553 (defaultFluctuatedAmperagePerPhase as number) ??
554 getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
555 currentMeasurandValues.L2 =
556 (phase2FluctuatedValue as number) ??
557 (defaultFluctuatedAmperagePerPhase as number) ??
558 getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
559 currentMeasurandValues.L3 =
560 (phase3FluctuatedValue as number) ??
561 (defaultFluctuatedAmperagePerPhase as number) ??
562 getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
563 } else {
564 currentMeasurandValues.L1 = currentSampledValueTemplate.value
565 ? getRandomFloatFluctuatedRounded(
566 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
567 currentSampledValueTemplate.value,
568 connectorMaximumAmperage,
569 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() },
570 ),
571 currentSampledValueTemplate.fluctuationPercent ??
572 Constants.DEFAULT_FLUCTUATION_PERCENT,
573 )
574 : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
575 currentMeasurandValues.L2 = 0;
576 currentMeasurandValues.L3 = 0;
577 }
578 currentMeasurandValues.allPhases = roundTo(
579 (currentMeasurandValues.L1 + currentMeasurandValues.L2 + currentMeasurandValues.L3) /
580 chargingStation.getNumberOfPhases(),
581 2,
582 );
583 break;
584 case CurrentType.DC:
585 connectorMaximumAmperage = DCElectricUtils.amperage(
586 connectorMaximumAvailablePower,
587 chargingStation.getVoltageOut(),
588 );
589 currentMeasurandValues.allPhases = currentSampledValueTemplate.value
590 ? getRandomFloatFluctuatedRounded(
591 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
592 currentSampledValueTemplate.value,
593 connectorMaximumAmperage,
594 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() },
595 ),
596 currentSampledValueTemplate.fluctuationPercent ??
597 Constants.DEFAULT_FLUCTUATION_PERCENT,
598 )
599 : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
600 break;
601 default:
602 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
603 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
604 }
605 meterValue.sampledValue.push(
606 OCPP16ServiceUtils.buildSampledValue(
607 currentSampledValueTemplate,
608 currentMeasurandValues.allPhases,
609 ),
610 );
611 const sampledValuesIndex = meterValue.sampledValue.length - 1;
612 if (
613 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
614 connectorMaximumAmperage ||
615 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) <
616 connectorMinimumAmperage ||
617 debug
618 ) {
619 logger.error(
620 `${chargingStation.logPrefix()} MeterValues measurand ${
621 meterValue.sampledValue[sampledValuesIndex].measurand ??
622 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
623 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${
624 meterValue.sampledValue[sampledValuesIndex].value
625 }/${connectorMaximumAmperage}`,
626 );
627 }
628 for (
629 let phase = 1;
630 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
631 phase++
632 ) {
633 const phaseValue = `L${phase}`;
634 meterValue.sampledValue.push(
635 OCPP16ServiceUtils.buildSampledValue(
636 currentPerPhaseSampledValueTemplates[
637 phaseValue as keyof MeasurandPerPhaseSampledValueTemplates
638 ]! ?? currentSampledValueTemplate,
639 currentMeasurandValues[phaseValue as keyof MeasurandPerPhaseSampledValueTemplates],
640 undefined,
641 phaseValue as OCPP16MeterValuePhase,
642 ),
643 );
644 const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1;
645 if (
646 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
647 connectorMaximumAmperage ||
648 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
649 connectorMinimumAmperage ||
650 debug
651 ) {
652 logger.error(
653 `${chargingStation.logPrefix()} MeterValues measurand ${
654 meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ??
655 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
656 }: phase ${
657 meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
658 }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${
659 meterValue.sampledValue[sampledValuesPerPhaseIndex].value
660 }/${connectorMaximumAmperage}`,
661 );
662 }
663 }
664 }
665 // Energy.Active.Import.Register measurand (default)
666 const energySampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
667 chargingStation,
668 connectorId,
669 );
670 if (energySampledValueTemplate) {
671 OCPP16ServiceUtils.checkMeasurandPowerDivider(
672 chargingStation,
673 energySampledValueTemplate.measurand!,
674 );
675 const unitDivider =
676 energySampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
677 const connectorMaximumAvailablePower =
678 chargingStation.getConnectorMaximumAvailablePower(connectorId);
679 const connectorMaximumEnergyRounded = roundTo(
680 (connectorMaximumAvailablePower * interval) / (3600 * 1000),
681 2,
682 );
683 const energyValueRounded = energySampledValueTemplate.value
684 ? // Cumulate the fluctuated value around the static one
685 getRandomFloatFluctuatedRounded(
686 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
687 energySampledValueTemplate.value,
688 connectorMaximumEnergyRounded,
689 {
690 limitationEnabled: chargingStation.getCustomValueLimitationMeterValues(),
691 unitMultiplier: unitDivider,
692 },
693 ),
694 energySampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT,
695 )
696 : getRandomFloatRounded(connectorMaximumEnergyRounded);
697 // Persist previous value on connector
698 if (connector) {
699 if (
700 isNullOrUndefined(connector.energyActiveImportRegisterValue) === false &&
701 connector.energyActiveImportRegisterValue! >= 0 &&
702 isNullOrUndefined(connector.transactionEnergyActiveImportRegisterValue) === false &&
703 connector.transactionEnergyActiveImportRegisterValue! >= 0
704 ) {
705 connector.energyActiveImportRegisterValue! += energyValueRounded;
706 connector.transactionEnergyActiveImportRegisterValue! += energyValueRounded;
707 } else {
708 connector.energyActiveImportRegisterValue = 0;
709 connector.transactionEnergyActiveImportRegisterValue = 0;
710 }
711 }
712 meterValue.sampledValue.push(
713 OCPP16ServiceUtils.buildSampledValue(
714 energySampledValueTemplate,
715 roundTo(
716 chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) /
717 unitDivider,
718 2,
719 ),
720 ),
721 );
722 const sampledValuesIndex = meterValue.sampledValue.length - 1;
723 if (energyValueRounded > connectorMaximumEnergyRounded || debug) {
724 logger.error(
725 `${chargingStation.logPrefix()} MeterValues measurand ${
726 meterValue.sampledValue[sampledValuesIndex].measurand ??
727 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
728 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${roundTo(
729 interval / (3600 * 1000),
730 4,
731 )}h`,
732 );
733 }
734 }
735 return meterValue;
736 }
737
738 public static buildTransactionBeginMeterValue(
739 chargingStation: ChargingStation,
740 connectorId: number,
741 meterStart: number,
742 ): OCPP16MeterValue {
743 const meterValue: OCPP16MeterValue = {
744 timestamp: new Date(),
745 sampledValue: [],
746 };
747 // Energy.Active.Import.Register measurand (default)
748 const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
749 chargingStation,
750 connectorId,
751 );
752 const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
753 meterValue.sampledValue.push(
754 OCPP16ServiceUtils.buildSampledValue(
755 sampledValueTemplate!,
756 roundTo((meterStart ?? 0) / unitDivider, 4),
757 MeterValueContext.TRANSACTION_BEGIN,
758 ),
759 );
760 return meterValue;
761 }
762
763 public static buildTransactionEndMeterValue(
764 chargingStation: ChargingStation,
765 connectorId: number,
766 meterStop: number,
767 ): OCPP16MeterValue {
768 const meterValue: OCPP16MeterValue = {
769 timestamp: new Date(),
770 sampledValue: [],
771 };
772 // Energy.Active.Import.Register measurand (default)
773 const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
774 chargingStation,
775 connectorId,
776 );
777 const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
778 meterValue.sampledValue.push(
779 OCPP16ServiceUtils.buildSampledValue(
780 sampledValueTemplate!,
781 roundTo((meterStop ?? 0) / unitDivider, 4),
782 MeterValueContext.TRANSACTION_END,
783 ),
784 );
785 return meterValue;
786 }
787
788 public static buildTransactionDataMeterValues(
789 transactionBeginMeterValue: OCPP16MeterValue,
790 transactionEndMeterValue: OCPP16MeterValue,
791 ): OCPP16MeterValue[] {
792 const meterValues: OCPP16MeterValue[] = [];
793 meterValues.push(transactionBeginMeterValue);
794 meterValues.push(transactionEndMeterValue);
795 return meterValues;
796 }
797
798 public static setChargingProfile(
799 chargingStation: ChargingStation,
800 connectorId: number,
801 cp: OCPP16ChargingProfile,
802 ): void {
803 if (isNullOrUndefined(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
804 logger.error(
805 `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an uninitialized charging profiles array attribute, applying deferred initialization`,
806 );
807 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = [];
808 }
809 if (
810 Array.isArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles) === false
811 ) {
812 logger.error(
813 `${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`,
814 );
815 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = [];
816 }
817 let cpReplaced = false;
818 if (isNotEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
819 chargingStation
820 .getConnectorStatus(connectorId)
821 ?.chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
822 if (
823 chargingProfile.chargingProfileId === cp.chargingProfileId ||
824 (chargingProfile.stackLevel === cp.stackLevel &&
825 chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)
826 ) {
827 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles![index] = cp;
828 cpReplaced = true;
829 }
830 });
831 }
832 !cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp);
833 }
834
835 public static parseJsonSchemaFile<T extends JsonType>(
836 relativePath: string,
837 moduleName?: string,
838 methodName?: string,
839 ): JSONSchemaType<T> {
840 return super.parseJsonSchemaFile<T>(
841 relativePath,
842 OCPPVersion.VERSION_16,
843 moduleName,
844 methodName,
845 );
846 }
847
848 public static async isIdTagAuthorized(
849 chargingStation: ChargingStation,
850 connectorId: number,
851 idTag: string,
852 ): Promise<boolean> {
853 let authorized = false;
854 const connectorStatus: ConnectorStatus = chargingStation.getConnectorStatus(connectorId)!;
855 if (OCPP16ServiceUtils.isIdTagLocalAuthorized(chargingStation, idTag)) {
856 connectorStatus.localAuthorizeIdTag = idTag;
857 connectorStatus.idTagLocalAuthorized = true;
858 authorized = true;
859 } else if (chargingStation.getMustAuthorizeAtRemoteStart() === true) {
860 connectorStatus.authorizeIdTag = idTag;
861 authorized = await OCPP16ServiceUtils.isIdTagRemoteAuthorized(chargingStation, idTag);
862 } else {
863 logger.warn(
864 `${chargingStation.logPrefix()} The charging station configuration expects authorize at
865 remote start transaction but local authorization or authorize isn't enabled`,
866 );
867 }
868 return authorized;
869 }
870
871 private static buildSampledValue(
872 sampledValueTemplate: SampledValueTemplate,
873 value: number,
874 context?: MeterValueContext,
875 phase?: OCPP16MeterValuePhase,
876 ): OCPP16SampledValue {
877 const sampledValueValue = value ?? sampledValueTemplate?.value ?? null;
878 const sampledValueContext = context ?? sampledValueTemplate?.context ?? null;
879 const sampledValueLocation =
880 sampledValueTemplate?.location ??
881 OCPP16ServiceUtils.getMeasurandDefaultLocation(sampledValueTemplate.measurand!);
882 const sampledValuePhase = phase ?? sampledValueTemplate?.phase ?? null;
883 return {
884 ...(!isNullOrUndefined(sampledValueTemplate.unit) && {
885 unit: sampledValueTemplate.unit,
886 }),
887 ...(!isNullOrUndefined(sampledValueContext) && { context: sampledValueContext }),
888 ...(!isNullOrUndefined(sampledValueTemplate.measurand) && {
889 measurand: sampledValueTemplate.measurand,
890 }),
891 ...(!isNullOrUndefined(sampledValueLocation) && { location: sampledValueLocation }),
892 ...(!isNullOrUndefined(sampledValueValue) && { value: sampledValueValue.toString() }),
893 ...(!isNullOrUndefined(sampledValuePhase) && { phase: sampledValuePhase }),
894 } as OCPP16SampledValue;
895 }
896
897 private static checkMeasurandPowerDivider(
898 chargingStation: ChargingStation,
899 measurandType: OCPP16MeterValueMeasurand,
900 ): void {
901 if (isUndefined(chargingStation.powerDivider)) {
902 const errMsg = `MeterValues measurand ${
903 measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
904 }: powerDivider is undefined`;
905 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
906 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
907 } else if (chargingStation?.powerDivider <= 0) {
908 const errMsg = `MeterValues measurand ${
909 measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
910 }: powerDivider have zero or below value ${chargingStation.powerDivider}`;
911 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
912 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
913 }
914 }
915
916 private static getMeasurandDefaultLocation(
917 measurandType: OCPP16MeterValueMeasurand,
918 ): MeterValueLocation | undefined {
919 switch (measurandType) {
920 case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
921 return MeterValueLocation.EV;
922 }
923 }
924
925 private static getMeasurandDefaultUnit(
926 measurandType: OCPP16MeterValueMeasurand,
927 ): MeterValueUnit | undefined {
928 switch (measurandType) {
929 case OCPP16MeterValueMeasurand.CURRENT_EXPORT:
930 case OCPP16MeterValueMeasurand.CURRENT_IMPORT:
931 case OCPP16MeterValueMeasurand.CURRENT_OFFERED:
932 return MeterValueUnit.AMP;
933 case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER:
934 case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER:
935 return MeterValueUnit.WATT_HOUR;
936 case OCPP16MeterValueMeasurand.POWER_ACTIVE_EXPORT:
937 case OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT:
938 case OCPP16MeterValueMeasurand.POWER_OFFERED:
939 return MeterValueUnit.WATT;
940 case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
941 return MeterValueUnit.PERCENT;
942 case OCPP16MeterValueMeasurand.VOLTAGE:
943 return MeterValueUnit.VOLT;
944 }
945 }
946
947 private static isIdTagLocalAuthorized(chargingStation: ChargingStation, idTag: string): boolean {
948 return (
949 chargingStation.getLocalAuthListEnabled() === true &&
950 chargingStation.hasIdTags() === true &&
951 isNotEmptyString(
952 chargingStation.idTagsCache
953 .getIdTags(getIdTagsFile(chargingStation.stationInfo)!)
954 ?.find((tag) => tag === idTag),
955 )
956 );
957 }
958
959 private static async isIdTagRemoteAuthorized(
960 chargingStation: ChargingStation,
961 idTag: string,
962 ): Promise<boolean> {
963 const authorizeResponse: OCPP16AuthorizeResponse =
964 await chargingStation.ocppRequestService.requestHandler<
965 OCPP16AuthorizeRequest,
966 OCPP16AuthorizeResponse
967 >(chargingStation, OCPP16RequestCommand.AUTHORIZE, {
968 idTag: idTag,
969 });
970 return authorizeResponse?.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED;
971 }
972 }