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