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