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