fix: avoid NaN at meterValues generation
[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 import {
5 addSeconds,
6 areIntervalsOverlapping,
7 differenceInSeconds,
8 isAfter,
9 isBefore,
10 isWithinInterval,
11 } from 'date-fns';
12
13 import { OCPP16Constants } from './OCPP16Constants';
14 import {
15 type ChargingStation,
16 hasFeatureProfile,
17 hasReservationExpired,
18 } from '../../../charging-station';
19 import { OCPPError } from '../../../exception';
20 import {
21 type ClearChargingProfileRequest,
22 CurrentType,
23 ErrorType,
24 type GenericResponse,
25 type JsonType,
26 type MeasurandPerPhaseSampledValueTemplates,
27 type MeasurandValues,
28 MeterValueContext,
29 MeterValueLocation,
30 MeterValueUnit,
31 OCPP16AuthorizationStatus,
32 OCPP16AvailabilityType,
33 type OCPP16ChangeAvailabilityResponse,
34 OCPP16ChargePointStatus,
35 type OCPP16ChargingProfile,
36 type OCPP16ChargingSchedule,
37 type OCPP16IncomingRequestCommand,
38 type OCPP16MeterValue,
39 OCPP16MeterValueMeasurand,
40 OCPP16MeterValuePhase,
41 OCPP16RequestCommand,
42 type OCPP16SampledValue,
43 OCPP16StandardParametersKey,
44 OCPP16StopTransactionReason,
45 type OCPP16SupportedFeatureProfiles,
46 OCPPVersion,
47 type SampledValueTemplate,
48 } from '../../../types';
49 import {
50 ACElectricUtils,
51 Constants,
52 DCElectricUtils,
53 convertToFloat,
54 convertToInt,
55 getRandomFloatFluctuatedRounded,
56 getRandomFloatRounded,
57 getRandomInteger,
58 isNotEmptyArray,
59 isNotEmptyString,
60 isNullOrUndefined,
61 isUndefined,
62 logger,
63 roundTo,
64 } from '../../../utils';
65 import { OCPPServiceUtils } from '../OCPPServiceUtils';
66
67 export class OCPP16ServiceUtils extends OCPPServiceUtils {
68 public static checkFeatureProfile(
69 chargingStation: ChargingStation,
70 featureProfile: OCPP16SupportedFeatureProfiles,
71 command: OCPP16RequestCommand | OCPP16IncomingRequestCommand,
72 ): boolean {
73 if (!hasFeatureProfile(chargingStation, featureProfile)) {
74 logger.warn(
75 `${chargingStation.logPrefix()} Trying to '${command}' without '${featureProfile}' feature enabled in ${
76 OCPP16StandardParametersKey.SupportedFeatureProfiles
77 } in configuration`,
78 );
79 return false;
80 }
81 return true;
82 }
83
84 public static buildMeterValue(
85 chargingStation: ChargingStation,
86 connectorId: number,
87 transactionId: number,
88 interval: number,
89 debug = false,
90 ): OCPP16MeterValue {
91 const meterValue: OCPP16MeterValue = {
92 timestamp: new Date(),
93 sampledValue: [],
94 };
95 const connector = chargingStation.getConnectorStatus(connectorId);
96 // SoC measurand
97 const socSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
98 chargingStation,
99 connectorId,
100 OCPP16MeterValueMeasurand.STATE_OF_CHARGE,
101 );
102 if (socSampledValueTemplate) {
103 const socMaximumValue = 100;
104 const socMinimumValue = socSampledValueTemplate.minimumValue ?? 0;
105 const socSampledValueTemplateValue = isNotEmptyString(socSampledValueTemplate.value)
106 ? getRandomFloatFluctuatedRounded(
107 parseInt(socSampledValueTemplate.value),
108 socSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT,
109 )
110 : getRandomInteger(socMaximumValue, socMinimumValue);
111 meterValue.sampledValue.push(
112 OCPP16ServiceUtils.buildSampledValue(socSampledValueTemplate, socSampledValueTemplateValue),
113 );
114 const sampledValuesIndex = meterValue.sampledValue.length - 1;
115 if (
116 convertToInt(meterValue.sampledValue[sampledValuesIndex].value) > socMaximumValue ||
117 convertToInt(meterValue.sampledValue[sampledValuesIndex].value) < socMinimumValue ||
118 debug
119 ) {
120 logger.error(
121 `${chargingStation.logPrefix()} MeterValues measurand ${
122 meterValue.sampledValue[sampledValuesIndex].measurand ??
123 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
124 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${socMinimumValue}/${
125 meterValue.sampledValue[sampledValuesIndex].value
126 }/${socMaximumValue}`,
127 );
128 }
129 }
130 // Voltage measurand
131 const voltageSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
132 chargingStation,
133 connectorId,
134 OCPP16MeterValueMeasurand.VOLTAGE,
135 );
136 if (voltageSampledValueTemplate) {
137 const voltageSampledValueTemplateValue = isNotEmptyString(voltageSampledValueTemplate.value)
138 ? parseInt(voltageSampledValueTemplate.value)
139 : chargingStation.stationInfo.voltageOut!;
140 const fluctuationPercent =
141 voltageSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT;
142 const voltageMeasurandValue = getRandomFloatFluctuatedRounded(
143 voltageSampledValueTemplateValue,
144 fluctuationPercent,
145 );
146 if (
147 chargingStation.getNumberOfPhases() !== 3 ||
148 (chargingStation.getNumberOfPhases() === 3 &&
149 chargingStation.stationInfo?.mainVoltageMeterValues)
150 ) {
151 meterValue.sampledValue.push(
152 OCPP16ServiceUtils.buildSampledValue(voltageSampledValueTemplate, voltageMeasurandValue),
153 );
154 }
155 for (
156 let phase = 1;
157 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
158 phase++
159 ) {
160 const phaseLineToNeutralValue = `L${phase}-N`;
161 const voltagePhaseLineToNeutralSampledValueTemplate =
162 OCPP16ServiceUtils.getSampledValueTemplate(
163 chargingStation,
164 connectorId,
165 OCPP16MeterValueMeasurand.VOLTAGE,
166 phaseLineToNeutralValue as OCPP16MeterValuePhase,
167 );
168 let voltagePhaseLineToNeutralMeasurandValue: number | undefined;
169 if (voltagePhaseLineToNeutralSampledValueTemplate) {
170 const voltagePhaseLineToNeutralSampledValueTemplateValue = isNotEmptyString(
171 voltagePhaseLineToNeutralSampledValueTemplate.value,
172 )
173 ? parseInt(voltagePhaseLineToNeutralSampledValueTemplate.value)
174 : chargingStation.stationInfo.voltageOut!;
175 const fluctuationPhaseToNeutralPercent =
176 voltagePhaseLineToNeutralSampledValueTemplate.fluctuationPercent ??
177 Constants.DEFAULT_FLUCTUATION_PERCENT;
178 voltagePhaseLineToNeutralMeasurandValue = getRandomFloatFluctuatedRounded(
179 voltagePhaseLineToNeutralSampledValueTemplateValue,
180 fluctuationPhaseToNeutralPercent,
181 );
182 }
183 meterValue.sampledValue.push(
184 OCPP16ServiceUtils.buildSampledValue(
185 voltagePhaseLineToNeutralSampledValueTemplate ?? voltageSampledValueTemplate,
186 voltagePhaseLineToNeutralMeasurandValue ?? voltageMeasurandValue,
187 undefined,
188 phaseLineToNeutralValue as OCPP16MeterValuePhase,
189 ),
190 );
191 if (chargingStation.stationInfo?.phaseLineToLineVoltageMeterValues) {
192 const phaseLineToLineValue = `L${phase}-L${
193 (phase + 1) % chargingStation.getNumberOfPhases() !== 0
194 ? (phase + 1) % chargingStation.getNumberOfPhases()
195 : chargingStation.getNumberOfPhases()
196 }`;
197 const voltagePhaseLineToLineSampledValueTemplate =
198 OCPP16ServiceUtils.getSampledValueTemplate(
199 chargingStation,
200 connectorId,
201 OCPP16MeterValueMeasurand.VOLTAGE,
202 phaseLineToLineValue as OCPP16MeterValuePhase,
203 );
204 let voltagePhaseLineToLineMeasurandValue: number | undefined;
205 if (voltagePhaseLineToLineSampledValueTemplate) {
206 const voltagePhaseLineToLineSampledValueTemplateValue = isNotEmptyString(
207 voltagePhaseLineToLineSampledValueTemplate.value,
208 )
209 ? parseInt(voltagePhaseLineToLineSampledValueTemplate.value)
210 : roundTo(
211 Math.sqrt(chargingStation.getNumberOfPhases()) *
212 chargingStation.stationInfo.voltageOut!,
213 2,
214 );
215 const fluctuationPhaseLineToLinePercent =
216 voltagePhaseLineToLineSampledValueTemplate.fluctuationPercent ??
217 Constants.DEFAULT_FLUCTUATION_PERCENT;
218 voltagePhaseLineToLineMeasurandValue = getRandomFloatFluctuatedRounded(
219 voltagePhaseLineToLineSampledValueTemplateValue,
220 fluctuationPhaseLineToLinePercent,
221 );
222 }
223 const defaultVoltagePhaseLineToLineMeasurandValue = getRandomFloatFluctuatedRounded(
224 Math.sqrt(chargingStation.getNumberOfPhases()) *
225 chargingStation.stationInfo.voltageOut!,
226 fluctuationPercent,
227 );
228 meterValue.sampledValue.push(
229 OCPP16ServiceUtils.buildSampledValue(
230 voltagePhaseLineToLineSampledValueTemplate ?? voltageSampledValueTemplate,
231 voltagePhaseLineToLineMeasurandValue ?? defaultVoltagePhaseLineToLineMeasurandValue,
232 undefined,
233 phaseLineToLineValue as OCPP16MeterValuePhase,
234 ),
235 );
236 }
237 }
238 }
239 // Power.Active.Import measurand
240 const powerSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
241 chargingStation,
242 connectorId,
243 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
244 );
245 let powerPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {};
246 if (chargingStation.getNumberOfPhases() === 3) {
247 powerPerPhaseSampledValueTemplates = {
248 L1: OCPP16ServiceUtils.getSampledValueTemplate(
249 chargingStation,
250 connectorId,
251 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
252 OCPP16MeterValuePhase.L1_N,
253 ),
254 L2: OCPP16ServiceUtils.getSampledValueTemplate(
255 chargingStation,
256 connectorId,
257 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
258 OCPP16MeterValuePhase.L2_N,
259 ),
260 L3: OCPP16ServiceUtils.getSampledValueTemplate(
261 chargingStation,
262 connectorId,
263 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
264 OCPP16MeterValuePhase.L3_N,
265 ),
266 };
267 }
268 if (powerSampledValueTemplate) {
269 OCPP16ServiceUtils.checkMeasurandPowerDivider(
270 chargingStation,
271 powerSampledValueTemplate.measurand!,
272 );
273 const errMsg = `MeterValues measurand ${
274 powerSampledValueTemplate.measurand ??
275 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
276 }: Unknown ${chargingStation.stationInfo?.currentOutType} currentOutType in template file ${
277 chargingStation.templateFile
278 }, cannot calculate ${
279 powerSampledValueTemplate.measurand ??
280 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
281 } measurand value`;
282 const powerMeasurandValues: MeasurandValues = {} as MeasurandValues;
283 const unitDivider = powerSampledValueTemplate?.unit === MeterValueUnit.KILO_WATT ? 1000 : 1;
284 const connectorMaximumAvailablePower =
285 chargingStation.getConnectorMaximumAvailablePower(connectorId);
286 const connectorMaximumPower = Math.round(connectorMaximumAvailablePower);
287 const connectorMaximumPowerPerPhase = Math.round(
288 connectorMaximumAvailablePower / chargingStation.getNumberOfPhases(),
289 );
290 const connectorMinimumPower = Math.round(powerSampledValueTemplate.minimumValue!) ?? 0;
291 const connectorMinimumPowerPerPhase = Math.round(
292 connectorMinimumPower / chargingStation.getNumberOfPhases(),
293 );
294 switch (chargingStation.stationInfo?.currentOutType) {
295 case CurrentType.AC:
296 if (chargingStation.getNumberOfPhases() === 3) {
297 const defaultFluctuatedPowerPerPhase =
298 powerSampledValueTemplate.value &&
299 getRandomFloatFluctuatedRounded(
300 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
301 powerSampledValueTemplate.value,
302 connectorMaximumPower / unitDivider,
303 {
304 limitationEnabled:
305 chargingStation.stationInfo?.customValueLimitationMeterValues,
306 },
307 ) / chargingStation.getNumberOfPhases(),
308 powerSampledValueTemplate.fluctuationPercent ??
309 Constants.DEFAULT_FLUCTUATION_PERCENT,
310 );
311 const phase1FluctuatedValue =
312 powerPerPhaseSampledValueTemplates.L1?.value &&
313 getRandomFloatFluctuatedRounded(
314 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
315 powerPerPhaseSampledValueTemplates.L1.value,
316 connectorMaximumPowerPerPhase / unitDivider,
317 {
318 limitationEnabled:
319 chargingStation.stationInfo?.customValueLimitationMeterValues,
320 },
321 ),
322 powerPerPhaseSampledValueTemplates.L1.fluctuationPercent ??
323 Constants.DEFAULT_FLUCTUATION_PERCENT,
324 );
325 const phase2FluctuatedValue =
326 powerPerPhaseSampledValueTemplates.L2?.value &&
327 getRandomFloatFluctuatedRounded(
328 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
329 powerPerPhaseSampledValueTemplates.L2.value,
330 connectorMaximumPowerPerPhase / unitDivider,
331 {
332 limitationEnabled:
333 chargingStation.stationInfo?.customValueLimitationMeterValues,
334 },
335 ),
336 powerPerPhaseSampledValueTemplates.L2.fluctuationPercent ??
337 Constants.DEFAULT_FLUCTUATION_PERCENT,
338 );
339 const phase3FluctuatedValue =
340 powerPerPhaseSampledValueTemplates.L3?.value &&
341 getRandomFloatFluctuatedRounded(
342 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
343 powerPerPhaseSampledValueTemplates.L3.value,
344 connectorMaximumPowerPerPhase / unitDivider,
345 {
346 limitationEnabled:
347 chargingStation.stationInfo?.customValueLimitationMeterValues,
348 },
349 ),
350 powerPerPhaseSampledValueTemplates.L3.fluctuationPercent ??
351 Constants.DEFAULT_FLUCTUATION_PERCENT,
352 );
353 powerMeasurandValues.L1 =
354 (phase1FluctuatedValue as number) ??
355 (defaultFluctuatedPowerPerPhase as number) ??
356 getRandomFloatRounded(
357 connectorMaximumPowerPerPhase / unitDivider,
358 connectorMinimumPowerPerPhase / unitDivider,
359 );
360 powerMeasurandValues.L2 =
361 (phase2FluctuatedValue as number) ??
362 (defaultFluctuatedPowerPerPhase as number) ??
363 getRandomFloatRounded(
364 connectorMaximumPowerPerPhase / unitDivider,
365 connectorMinimumPowerPerPhase / unitDivider,
366 );
367 powerMeasurandValues.L3 =
368 (phase3FluctuatedValue as number) ??
369 (defaultFluctuatedPowerPerPhase as number) ??
370 getRandomFloatRounded(
371 connectorMaximumPowerPerPhase / unitDivider,
372 connectorMinimumPowerPerPhase / unitDivider,
373 );
374 } else {
375 powerMeasurandValues.L1 = powerSampledValueTemplate.value
376 ? getRandomFloatFluctuatedRounded(
377 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
378 powerSampledValueTemplate.value,
379 connectorMaximumPower / unitDivider,
380 {
381 limitationEnabled:
382 chargingStation.stationInfo?.customValueLimitationMeterValues,
383 },
384 ),
385 powerSampledValueTemplate.fluctuationPercent ??
386 Constants.DEFAULT_FLUCTUATION_PERCENT,
387 )
388 : getRandomFloatRounded(
389 connectorMaximumPower / unitDivider,
390 connectorMinimumPower / unitDivider,
391 );
392 powerMeasurandValues.L2 = 0;
393 powerMeasurandValues.L3 = 0;
394 }
395 powerMeasurandValues.allPhases = roundTo(
396 powerMeasurandValues.L1 + powerMeasurandValues.L2 + powerMeasurandValues.L3,
397 2,
398 );
399 break;
400 case CurrentType.DC:
401 powerMeasurandValues.allPhases = isNotEmptyString(powerSampledValueTemplate.value)
402 ? getRandomFloatFluctuatedRounded(
403 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
404 powerSampledValueTemplate.value,
405 connectorMaximumPower / unitDivider,
406 {
407 limitationEnabled:
408 chargingStation.stationInfo?.customValueLimitationMeterValues,
409 },
410 ),
411 powerSampledValueTemplate.fluctuationPercent ??
412 Constants.DEFAULT_FLUCTUATION_PERCENT,
413 )
414 : getRandomFloatRounded(
415 connectorMaximumPower / unitDivider,
416 connectorMinimumPower / unitDivider,
417 );
418 break;
419 default:
420 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
421 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
422 }
423 meterValue.sampledValue.push(
424 OCPP16ServiceUtils.buildSampledValue(
425 powerSampledValueTemplate,
426 powerMeasurandValues.allPhases,
427 ),
428 );
429 const sampledValuesIndex = meterValue.sampledValue.length - 1;
430 const connectorMaximumPowerRounded = roundTo(connectorMaximumPower / unitDivider, 2);
431 const connectorMinimumPowerRounded = roundTo(connectorMinimumPower / unitDivider, 2);
432 if (
433 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
434 connectorMaximumPowerRounded ||
435 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) <
436 connectorMinimumPowerRounded ||
437 debug
438 ) {
439 logger.error(
440 `${chargingStation.logPrefix()} MeterValues measurand ${
441 meterValue.sampledValue[sampledValuesIndex].measurand ??
442 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
443 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumPowerRounded}/${
444 meterValue.sampledValue[sampledValuesIndex].value
445 }/${connectorMaximumPowerRounded}`,
446 );
447 }
448 for (
449 let phase = 1;
450 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
451 phase++
452 ) {
453 const phaseValue = `L${phase}-N`;
454 meterValue.sampledValue.push(
455 OCPP16ServiceUtils.buildSampledValue(
456 powerPerPhaseSampledValueTemplates[
457 `L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates
458 ]! ?? powerSampledValueTemplate,
459 powerMeasurandValues[`L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates],
460 undefined,
461 phaseValue as OCPP16MeterValuePhase,
462 ),
463 );
464 const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1;
465 const connectorMaximumPowerPerPhaseRounded = roundTo(
466 connectorMaximumPowerPerPhase / unitDivider,
467 2,
468 );
469 const connectorMinimumPowerPerPhaseRounded = roundTo(
470 connectorMinimumPowerPerPhase / unitDivider,
471 2,
472 );
473 if (
474 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
475 connectorMaximumPowerPerPhaseRounded ||
476 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
477 connectorMinimumPowerPerPhaseRounded ||
478 debug
479 ) {
480 logger.error(
481 `${chargingStation.logPrefix()} MeterValues measurand ${
482 meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ??
483 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
484 }: phase ${
485 meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
486 }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumPowerPerPhaseRounded}/${
487 meterValue.sampledValue[sampledValuesPerPhaseIndex].value
488 }/${connectorMaximumPowerPerPhaseRounded}`,
489 );
490 }
491 }
492 }
493 // Current.Import measurand
494 const currentSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
495 chargingStation,
496 connectorId,
497 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
498 );
499 let currentPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {};
500 if (chargingStation.getNumberOfPhases() === 3) {
501 currentPerPhaseSampledValueTemplates = {
502 L1: OCPP16ServiceUtils.getSampledValueTemplate(
503 chargingStation,
504 connectorId,
505 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
506 OCPP16MeterValuePhase.L1,
507 ),
508 L2: OCPP16ServiceUtils.getSampledValueTemplate(
509 chargingStation,
510 connectorId,
511 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
512 OCPP16MeterValuePhase.L2,
513 ),
514 L3: OCPP16ServiceUtils.getSampledValueTemplate(
515 chargingStation,
516 connectorId,
517 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
518 OCPP16MeterValuePhase.L3,
519 ),
520 };
521 }
522 if (currentSampledValueTemplate) {
523 OCPP16ServiceUtils.checkMeasurandPowerDivider(
524 chargingStation,
525 currentSampledValueTemplate.measurand!,
526 );
527 const errMsg = `MeterValues measurand ${
528 currentSampledValueTemplate.measurand ??
529 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
530 }: Unknown ${chargingStation.stationInfo?.currentOutType} currentOutType in template file ${
531 chargingStation.templateFile
532 }, cannot calculate ${
533 currentSampledValueTemplate.measurand ??
534 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
535 } measurand value`;
536 const currentMeasurandValues: MeasurandValues = {} as MeasurandValues;
537 const connectorMaximumAvailablePower =
538 chargingStation.getConnectorMaximumAvailablePower(connectorId);
539 const connectorMinimumAmperage = currentSampledValueTemplate.minimumValue ?? 0;
540 let connectorMaximumAmperage: number;
541 switch (chargingStation.stationInfo?.currentOutType) {
542 case CurrentType.AC:
543 connectorMaximumAmperage = ACElectricUtils.amperagePerPhaseFromPower(
544 chargingStation.getNumberOfPhases(),
545 connectorMaximumAvailablePower,
546 chargingStation.stationInfo.voltageOut!,
547 );
548 if (chargingStation.getNumberOfPhases() === 3) {
549 const defaultFluctuatedAmperagePerPhase =
550 currentSampledValueTemplate.value &&
551 getRandomFloatFluctuatedRounded(
552 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
553 currentSampledValueTemplate.value,
554 connectorMaximumAmperage,
555 {
556 limitationEnabled:
557 chargingStation.stationInfo?.customValueLimitationMeterValues,
558 },
559 ),
560 currentSampledValueTemplate.fluctuationPercent ??
561 Constants.DEFAULT_FLUCTUATION_PERCENT,
562 );
563 const phase1FluctuatedValue =
564 currentPerPhaseSampledValueTemplates.L1?.value &&
565 getRandomFloatFluctuatedRounded(
566 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
567 currentPerPhaseSampledValueTemplates.L1.value,
568 connectorMaximumAmperage,
569 {
570 limitationEnabled:
571 chargingStation.stationInfo?.customValueLimitationMeterValues,
572 },
573 ),
574 currentPerPhaseSampledValueTemplates.L1.fluctuationPercent ??
575 Constants.DEFAULT_FLUCTUATION_PERCENT,
576 );
577 const phase2FluctuatedValue =
578 currentPerPhaseSampledValueTemplates.L2?.value &&
579 getRandomFloatFluctuatedRounded(
580 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
581 currentPerPhaseSampledValueTemplates.L2.value,
582 connectorMaximumAmperage,
583 {
584 limitationEnabled:
585 chargingStation.stationInfo?.customValueLimitationMeterValues,
586 },
587 ),
588 currentPerPhaseSampledValueTemplates.L2.fluctuationPercent ??
589 Constants.DEFAULT_FLUCTUATION_PERCENT,
590 );
591 const phase3FluctuatedValue =
592 currentPerPhaseSampledValueTemplates.L3?.value &&
593 getRandomFloatFluctuatedRounded(
594 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
595 currentPerPhaseSampledValueTemplates.L3.value,
596 connectorMaximumAmperage,
597 {
598 limitationEnabled:
599 chargingStation.stationInfo?.customValueLimitationMeterValues,
600 },
601 ),
602 currentPerPhaseSampledValueTemplates.L3.fluctuationPercent ??
603 Constants.DEFAULT_FLUCTUATION_PERCENT,
604 );
605 currentMeasurandValues.L1 =
606 (phase1FluctuatedValue as number) ??
607 (defaultFluctuatedAmperagePerPhase as number) ??
608 getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
609 currentMeasurandValues.L2 =
610 (phase2FluctuatedValue as number) ??
611 (defaultFluctuatedAmperagePerPhase as number) ??
612 getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
613 currentMeasurandValues.L3 =
614 (phase3FluctuatedValue as number) ??
615 (defaultFluctuatedAmperagePerPhase as number) ??
616 getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
617 } else {
618 currentMeasurandValues.L1 = isNotEmptyString(currentSampledValueTemplate.value)
619 ? getRandomFloatFluctuatedRounded(
620 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
621 currentSampledValueTemplate.value,
622 connectorMaximumAmperage,
623 {
624 limitationEnabled:
625 chargingStation.stationInfo?.customValueLimitationMeterValues,
626 },
627 ),
628 currentSampledValueTemplate.fluctuationPercent ??
629 Constants.DEFAULT_FLUCTUATION_PERCENT,
630 )
631 : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
632 currentMeasurandValues.L2 = 0;
633 currentMeasurandValues.L3 = 0;
634 }
635 currentMeasurandValues.allPhases = roundTo(
636 (currentMeasurandValues.L1 + currentMeasurandValues.L2 + currentMeasurandValues.L3) /
637 chargingStation.getNumberOfPhases(),
638 2,
639 );
640 break;
641 case CurrentType.DC:
642 connectorMaximumAmperage = DCElectricUtils.amperage(
643 connectorMaximumAvailablePower,
644 chargingStation.stationInfo.voltageOut!,
645 );
646 currentMeasurandValues.allPhases = isNotEmptyString(currentSampledValueTemplate.value)
647 ? getRandomFloatFluctuatedRounded(
648 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
649 currentSampledValueTemplate.value,
650 connectorMaximumAmperage,
651 {
652 limitationEnabled:
653 chargingStation.stationInfo?.customValueLimitationMeterValues,
654 },
655 ),
656 currentSampledValueTemplate.fluctuationPercent ??
657 Constants.DEFAULT_FLUCTUATION_PERCENT,
658 )
659 : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
660 break;
661 default:
662 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
663 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
664 }
665 meterValue.sampledValue.push(
666 OCPP16ServiceUtils.buildSampledValue(
667 currentSampledValueTemplate,
668 currentMeasurandValues.allPhases,
669 ),
670 );
671 const sampledValuesIndex = meterValue.sampledValue.length - 1;
672 if (
673 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
674 connectorMaximumAmperage ||
675 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) <
676 connectorMinimumAmperage ||
677 debug
678 ) {
679 logger.error(
680 `${chargingStation.logPrefix()} MeterValues measurand ${
681 meterValue.sampledValue[sampledValuesIndex].measurand ??
682 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
683 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${
684 meterValue.sampledValue[sampledValuesIndex].value
685 }/${connectorMaximumAmperage}`,
686 );
687 }
688 for (
689 let phase = 1;
690 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
691 phase++
692 ) {
693 const phaseValue = `L${phase}`;
694 meterValue.sampledValue.push(
695 OCPP16ServiceUtils.buildSampledValue(
696 currentPerPhaseSampledValueTemplates[
697 phaseValue as keyof MeasurandPerPhaseSampledValueTemplates
698 ]! ?? currentSampledValueTemplate,
699 currentMeasurandValues[phaseValue as keyof MeasurandPerPhaseSampledValueTemplates],
700 undefined,
701 phaseValue as OCPP16MeterValuePhase,
702 ),
703 );
704 const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1;
705 if (
706 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
707 connectorMaximumAmperage ||
708 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
709 connectorMinimumAmperage ||
710 debug
711 ) {
712 logger.error(
713 `${chargingStation.logPrefix()} MeterValues measurand ${
714 meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ??
715 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
716 }: phase ${
717 meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
718 }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${
719 meterValue.sampledValue[sampledValuesPerPhaseIndex].value
720 }/${connectorMaximumAmperage}`,
721 );
722 }
723 }
724 }
725 // Energy.Active.Import.Register measurand (default)
726 const energySampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
727 chargingStation,
728 connectorId,
729 );
730 if (energySampledValueTemplate) {
731 OCPP16ServiceUtils.checkMeasurandPowerDivider(
732 chargingStation,
733 energySampledValueTemplate.measurand!,
734 );
735 const unitDivider =
736 energySampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
737 const connectorMaximumAvailablePower =
738 chargingStation.getConnectorMaximumAvailablePower(connectorId);
739 const connectorMaximumEnergyRounded = roundTo(
740 (connectorMaximumAvailablePower * interval) / (3600 * 1000),
741 2,
742 );
743 const energyValueRounded = energySampledValueTemplate.value
744 ? // Cumulate the fluctuated value around the static one
745 getRandomFloatFluctuatedRounded(
746 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
747 energySampledValueTemplate.value,
748 connectorMaximumEnergyRounded,
749 {
750 limitationEnabled: chargingStation.stationInfo?.customValueLimitationMeterValues,
751 unitMultiplier: unitDivider,
752 },
753 ),
754 energySampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT,
755 )
756 : getRandomFloatRounded(connectorMaximumEnergyRounded);
757 // Persist previous value on connector
758 if (connector) {
759 if (
760 isNullOrUndefined(connector.energyActiveImportRegisterValue) === false &&
761 connector.energyActiveImportRegisterValue! >= 0 &&
762 isNullOrUndefined(connector.transactionEnergyActiveImportRegisterValue) === false &&
763 connector.transactionEnergyActiveImportRegisterValue! >= 0
764 ) {
765 connector.energyActiveImportRegisterValue! += energyValueRounded;
766 connector.transactionEnergyActiveImportRegisterValue! += energyValueRounded;
767 } else {
768 connector.energyActiveImportRegisterValue = 0;
769 connector.transactionEnergyActiveImportRegisterValue = 0;
770 }
771 }
772 meterValue.sampledValue.push(
773 OCPP16ServiceUtils.buildSampledValue(
774 energySampledValueTemplate,
775 roundTo(
776 chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) /
777 unitDivider,
778 2,
779 ),
780 ),
781 );
782 const sampledValuesIndex = meterValue.sampledValue.length - 1;
783 if (energyValueRounded > connectorMaximumEnergyRounded || debug) {
784 logger.error(
785 `${chargingStation.logPrefix()} MeterValues measurand ${
786 meterValue.sampledValue[sampledValuesIndex].measurand ??
787 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
788 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${interval}ms`,
789 );
790 }
791 }
792 return meterValue;
793 }
794
795 public static buildTransactionBeginMeterValue(
796 chargingStation: ChargingStation,
797 connectorId: number,
798 meterStart: number,
799 ): OCPP16MeterValue {
800 const meterValue: OCPP16MeterValue = {
801 timestamp: new Date(),
802 sampledValue: [],
803 };
804 // Energy.Active.Import.Register measurand (default)
805 const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
806 chargingStation,
807 connectorId,
808 );
809 const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
810 meterValue.sampledValue.push(
811 OCPP16ServiceUtils.buildSampledValue(
812 sampledValueTemplate!,
813 roundTo((meterStart ?? 0) / unitDivider, 4),
814 MeterValueContext.TRANSACTION_BEGIN,
815 ),
816 );
817 return meterValue;
818 }
819
820 public static buildTransactionEndMeterValue(
821 chargingStation: ChargingStation,
822 connectorId: number,
823 meterStop: number,
824 ): OCPP16MeterValue {
825 const meterValue: OCPP16MeterValue = {
826 timestamp: new Date(),
827 sampledValue: [],
828 };
829 // Energy.Active.Import.Register measurand (default)
830 const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
831 chargingStation,
832 connectorId,
833 );
834 const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
835 meterValue.sampledValue.push(
836 OCPP16ServiceUtils.buildSampledValue(
837 sampledValueTemplate!,
838 roundTo((meterStop ?? 0) / unitDivider, 4),
839 MeterValueContext.TRANSACTION_END,
840 ),
841 );
842 return meterValue;
843 }
844
845 public static buildTransactionDataMeterValues(
846 transactionBeginMeterValue: OCPP16MeterValue,
847 transactionEndMeterValue: OCPP16MeterValue,
848 ): OCPP16MeterValue[] {
849 const meterValues: OCPP16MeterValue[] = [];
850 meterValues.push(transactionBeginMeterValue);
851 meterValues.push(transactionEndMeterValue);
852 return meterValues;
853 }
854
855 public static remoteStopTransaction = async (
856 chargingStation: ChargingStation,
857 connectorId: number,
858 ): Promise<GenericResponse> => {
859 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
860 chargingStation,
861 connectorId,
862 OCPP16ChargePointStatus.Finishing,
863 );
864 const stopResponse = await chargingStation.stopTransactionOnConnector(
865 connectorId,
866 OCPP16StopTransactionReason.REMOTE,
867 );
868 if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
869 return OCPP16Constants.OCPP_RESPONSE_ACCEPTED;
870 }
871 return OCPP16Constants.OCPP_RESPONSE_REJECTED;
872 };
873
874 public static changeAvailability = async (
875 chargingStation: ChargingStation,
876 connectorIds: number[],
877 chargePointStatus: OCPP16ChargePointStatus,
878 availabilityType: OCPP16AvailabilityType,
879 ): Promise<OCPP16ChangeAvailabilityResponse> => {
880 const responses: OCPP16ChangeAvailabilityResponse[] = [];
881 for (const connectorId of connectorIds) {
882 let response: OCPP16ChangeAvailabilityResponse =
883 OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
884 const connectorStatus = chargingStation.getConnectorStatus(connectorId)!;
885 if (connectorStatus?.transactionStarted === true) {
886 response = OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
887 }
888 connectorStatus.availability = availabilityType;
889 if (response === OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED) {
890 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
891 chargingStation,
892 connectorId,
893 chargePointStatus,
894 );
895 }
896 responses.push(response);
897 }
898 if (responses.includes(OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED)) {
899 return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
900 }
901 return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
902 };
903
904 public static setChargingProfile(
905 chargingStation: ChargingStation,
906 connectorId: number,
907 cp: OCPP16ChargingProfile,
908 ): void {
909 if (isNullOrUndefined(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
910 logger.error(
911 `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an uninitialized charging profiles array attribute, applying deferred initialization`,
912 );
913 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = [];
914 }
915 if (
916 Array.isArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles) === false
917 ) {
918 logger.error(
919 `${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 deferred initialization`,
920 );
921 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = [];
922 }
923 let cpReplaced = false;
924 if (isNotEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
925 chargingStation
926 .getConnectorStatus(connectorId)
927 ?.chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
928 if (
929 chargingProfile.chargingProfileId === cp.chargingProfileId ||
930 (chargingProfile.stackLevel === cp.stackLevel &&
931 chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)
932 ) {
933 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles![index] = cp;
934 cpReplaced = true;
935 }
936 });
937 }
938 !cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp);
939 }
940
941 public static clearChargingProfiles = (
942 chargingStation: ChargingStation,
943 commandPayload: ClearChargingProfileRequest,
944 chargingProfiles: OCPP16ChargingProfile[] | undefined,
945 ): boolean => {
946 const { id, chargingProfilePurpose, stackLevel } = commandPayload;
947 let clearedCP = false;
948 if (isNotEmptyArray(chargingProfiles)) {
949 chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
950 let clearCurrentCP = false;
951 if (chargingProfile.chargingProfileId === id) {
952 clearCurrentCP = true;
953 }
954 if (!chargingProfilePurpose && chargingProfile.stackLevel === stackLevel) {
955 clearCurrentCP = true;
956 }
957 if (!stackLevel && chargingProfile.chargingProfilePurpose === chargingProfilePurpose) {
958 clearCurrentCP = true;
959 }
960 if (
961 chargingProfile.stackLevel === stackLevel &&
962 chargingProfile.chargingProfilePurpose === chargingProfilePurpose
963 ) {
964 clearCurrentCP = true;
965 }
966 if (clearCurrentCP) {
967 chargingProfiles.splice(index, 1);
968 logger.debug(
969 `${chargingStation.logPrefix()} Matching charging profile(s) cleared: %j`,
970 chargingProfile,
971 );
972 clearedCP = true;
973 }
974 });
975 }
976 return clearedCP;
977 };
978
979 public static composeChargingSchedules = (
980 chargingScheduleHigher: OCPP16ChargingSchedule | undefined,
981 chargingScheduleLower: OCPP16ChargingSchedule | undefined,
982 compositeInterval: Interval,
983 ): OCPP16ChargingSchedule | undefined => {
984 if (!chargingScheduleHigher && !chargingScheduleLower) {
985 return undefined;
986 }
987 if (chargingScheduleHigher && !chargingScheduleLower) {
988 return OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleHigher, compositeInterval);
989 }
990 if (!chargingScheduleHigher && chargingScheduleLower) {
991 return OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleLower, compositeInterval);
992 }
993 const compositeChargingScheduleHigher: OCPP16ChargingSchedule | undefined =
994 OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleHigher!, compositeInterval);
995 const compositeChargingScheduleLower: OCPP16ChargingSchedule | undefined =
996 OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleLower!, compositeInterval);
997 const compositeChargingScheduleHigherInterval: Interval = {
998 start: compositeChargingScheduleHigher!.startSchedule!,
999 end: addSeconds(
1000 compositeChargingScheduleHigher!.startSchedule!,
1001 compositeChargingScheduleHigher!.duration!,
1002 ),
1003 };
1004 const compositeChargingScheduleLowerInterval: Interval = {
1005 start: compositeChargingScheduleLower!.startSchedule!,
1006 end: addSeconds(
1007 compositeChargingScheduleLower!.startSchedule!,
1008 compositeChargingScheduleLower!.duration!,
1009 ),
1010 };
1011 const higherFirst = isBefore(
1012 compositeChargingScheduleHigherInterval.start,
1013 compositeChargingScheduleLowerInterval.start,
1014 );
1015 if (
1016 !areIntervalsOverlapping(
1017 compositeChargingScheduleHigherInterval,
1018 compositeChargingScheduleLowerInterval,
1019 )
1020 ) {
1021 return {
1022 ...compositeChargingScheduleLower,
1023 ...compositeChargingScheduleHigher!,
1024 startSchedule: higherFirst
1025 ? (compositeChargingScheduleHigherInterval.start as Date)
1026 : (compositeChargingScheduleLowerInterval.start as Date),
1027 duration: higherFirst
1028 ? differenceInSeconds(
1029 compositeChargingScheduleLowerInterval.end,
1030 compositeChargingScheduleHigherInterval.start,
1031 )
1032 : differenceInSeconds(
1033 compositeChargingScheduleHigherInterval.end,
1034 compositeChargingScheduleLowerInterval.start,
1035 ),
1036 chargingSchedulePeriod: [
1037 ...compositeChargingScheduleHigher!.chargingSchedulePeriod.map((schedulePeriod) => {
1038 return {
1039 ...schedulePeriod,
1040 startPeriod: higherFirst
1041 ? 0
1042 : schedulePeriod.startPeriod +
1043 differenceInSeconds(
1044 compositeChargingScheduleHigherInterval.start,
1045 compositeChargingScheduleLowerInterval.start,
1046 ),
1047 };
1048 }),
1049 ...compositeChargingScheduleLower!.chargingSchedulePeriod.map((schedulePeriod) => {
1050 return {
1051 ...schedulePeriod,
1052 startPeriod: higherFirst
1053 ? schedulePeriod.startPeriod +
1054 differenceInSeconds(
1055 compositeChargingScheduleLowerInterval.start,
1056 compositeChargingScheduleHigherInterval.start,
1057 )
1058 : 0,
1059 };
1060 }),
1061 ].sort((a, b) => a.startPeriod - b.startPeriod),
1062 };
1063 }
1064 return {
1065 ...compositeChargingScheduleLower,
1066 ...compositeChargingScheduleHigher!,
1067 startSchedule: higherFirst
1068 ? (compositeChargingScheduleHigherInterval.start as Date)
1069 : (compositeChargingScheduleLowerInterval.start as Date),
1070 duration: higherFirst
1071 ? differenceInSeconds(
1072 compositeChargingScheduleLowerInterval.end,
1073 compositeChargingScheduleHigherInterval.start,
1074 )
1075 : differenceInSeconds(
1076 compositeChargingScheduleHigherInterval.end,
1077 compositeChargingScheduleLowerInterval.start,
1078 ),
1079 chargingSchedulePeriod: [
1080 ...compositeChargingScheduleHigher!.chargingSchedulePeriod.map((schedulePeriod) => {
1081 return {
1082 ...schedulePeriod,
1083 startPeriod: higherFirst
1084 ? 0
1085 : schedulePeriod.startPeriod +
1086 differenceInSeconds(
1087 compositeChargingScheduleHigherInterval.start,
1088 compositeChargingScheduleLowerInterval.start,
1089 ),
1090 };
1091 }),
1092 ...compositeChargingScheduleLower!.chargingSchedulePeriod
1093 .filter((schedulePeriod, index) => {
1094 if (
1095 higherFirst &&
1096 isWithinInterval(
1097 addSeconds(
1098 compositeChargingScheduleLowerInterval.start,
1099 schedulePeriod.startPeriod,
1100 ),
1101 {
1102 start: compositeChargingScheduleLowerInterval.start,
1103 end: compositeChargingScheduleHigherInterval.end,
1104 },
1105 )
1106 ) {
1107 return false;
1108 }
1109 if (
1110 higherFirst &&
1111 index < compositeChargingScheduleLower!.chargingSchedulePeriod.length - 1 &&
1112 !isWithinInterval(
1113 addSeconds(
1114 compositeChargingScheduleLowerInterval.start,
1115 schedulePeriod.startPeriod,
1116 ),
1117 {
1118 start: compositeChargingScheduleLowerInterval.start,
1119 end: compositeChargingScheduleHigherInterval.end,
1120 },
1121 ) &&
1122 isWithinInterval(
1123 addSeconds(
1124 compositeChargingScheduleLowerInterval.start,
1125 compositeChargingScheduleLower!.chargingSchedulePeriod[index + 1].startPeriod,
1126 ),
1127 {
1128 start: compositeChargingScheduleLowerInterval.start,
1129 end: compositeChargingScheduleHigherInterval.end,
1130 },
1131 )
1132 ) {
1133 return false;
1134 }
1135 if (
1136 !higherFirst &&
1137 isWithinInterval(
1138 addSeconds(
1139 compositeChargingScheduleLowerInterval.start,
1140 schedulePeriod.startPeriod,
1141 ),
1142 {
1143 start: compositeChargingScheduleHigherInterval.start,
1144 end: compositeChargingScheduleLowerInterval.end,
1145 },
1146 )
1147 ) {
1148 return false;
1149 }
1150 return true;
1151 })
1152 .map((schedulePeriod, index) => {
1153 if (index === 0 && schedulePeriod.startPeriod !== 0) {
1154 schedulePeriod.startPeriod = 0;
1155 }
1156 return {
1157 ...schedulePeriod,
1158 startPeriod: higherFirst
1159 ? schedulePeriod.startPeriod +
1160 differenceInSeconds(
1161 compositeChargingScheduleLowerInterval.start,
1162 compositeChargingScheduleHigherInterval.start,
1163 )
1164 : 0,
1165 };
1166 }),
1167 ].sort((a, b) => a.startPeriod - b.startPeriod),
1168 };
1169 };
1170
1171 public static hasReservation = (
1172 chargingStation: ChargingStation,
1173 connectorId: number,
1174 idTag: string,
1175 ): boolean => {
1176 const connectorReservation = chargingStation.getReservationBy('connectorId', connectorId);
1177 const chargingStationReservation = chargingStation.getReservationBy('connectorId', 0);
1178 if (
1179 (chargingStation.getConnectorStatus(connectorId)?.status ===
1180 OCPP16ChargePointStatus.Reserved &&
1181 connectorReservation &&
1182 !hasReservationExpired(connectorReservation) &&
1183 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
1184 connectorReservation?.idTag === idTag) ||
1185 (chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved &&
1186 chargingStationReservation &&
1187 !hasReservationExpired(chargingStationReservation) &&
1188 chargingStationReservation?.idTag === idTag)
1189 ) {
1190 logger.debug(
1191 `${chargingStation.logPrefix()} Connector id ${connectorId} has a valid reservation for idTag ${idTag}: %j`,
1192 connectorReservation ?? chargingStationReservation,
1193 );
1194 return true;
1195 }
1196 return false;
1197 };
1198
1199 public static parseJsonSchemaFile<T extends JsonType>(
1200 relativePath: string,
1201 moduleName?: string,
1202 methodName?: string,
1203 ): JSONSchemaType<T> {
1204 return super.parseJsonSchemaFile<T>(
1205 relativePath,
1206 OCPPVersion.VERSION_16,
1207 moduleName,
1208 methodName,
1209 );
1210 }
1211
1212 private static composeChargingSchedule = (
1213 chargingSchedule: OCPP16ChargingSchedule,
1214 compositeInterval: Interval,
1215 ): OCPP16ChargingSchedule | undefined => {
1216 const chargingScheduleInterval: Interval = {
1217 start: chargingSchedule.startSchedule!,
1218 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
1219 };
1220 if (areIntervalsOverlapping(chargingScheduleInterval, compositeInterval)) {
1221 chargingSchedule.chargingSchedulePeriod.sort((a, b) => a.startPeriod - b.startPeriod);
1222 if (isBefore(chargingScheduleInterval.start, compositeInterval.start)) {
1223 return {
1224 ...chargingSchedule,
1225 startSchedule: compositeInterval.start as Date,
1226 duration: differenceInSeconds(
1227 chargingScheduleInterval.end,
1228 compositeInterval.start as Date,
1229 ),
1230 chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod
1231 .filter((schedulePeriod, index) => {
1232 if (
1233 isWithinInterval(
1234 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
1235 compositeInterval,
1236 )
1237 ) {
1238 return true;
1239 }
1240 if (
1241 index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
1242 !isWithinInterval(
1243 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod),
1244 compositeInterval,
1245 ) &&
1246 isWithinInterval(
1247 addSeconds(
1248 chargingScheduleInterval.start,
1249 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
1250 ),
1251 compositeInterval,
1252 )
1253 ) {
1254 return true;
1255 }
1256 return false;
1257 })
1258 .map((schedulePeriod, index) => {
1259 if (index === 0 && schedulePeriod.startPeriod !== 0) {
1260 schedulePeriod.startPeriod = 0;
1261 }
1262 return schedulePeriod;
1263 }),
1264 };
1265 }
1266 if (isAfter(chargingScheduleInterval.end, compositeInterval.end)) {
1267 return {
1268 ...chargingSchedule,
1269 duration: differenceInSeconds(
1270 compositeInterval.end as Date,
1271 chargingScheduleInterval.start,
1272 ),
1273 chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter((schedulePeriod) =>
1274 isWithinInterval(
1275 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
1276 compositeInterval,
1277 ),
1278 ),
1279 };
1280 }
1281 return chargingSchedule;
1282 }
1283 };
1284
1285 private static buildSampledValue(
1286 sampledValueTemplate: SampledValueTemplate,
1287 value: number,
1288 context?: MeterValueContext,
1289 phase?: OCPP16MeterValuePhase,
1290 ): OCPP16SampledValue {
1291 const sampledValueValue = value ?? sampledValueTemplate?.value;
1292 const sampledValueContext = context ?? sampledValueTemplate?.context;
1293 const sampledValueLocation =
1294 sampledValueTemplate?.location ??
1295 OCPP16ServiceUtils.getMeasurandDefaultLocation(sampledValueTemplate.measurand!);
1296 const sampledValuePhase = phase ?? sampledValueTemplate?.phase;
1297 return {
1298 ...(!isNullOrUndefined(sampledValueTemplate.unit) && {
1299 unit: sampledValueTemplate.unit,
1300 }),
1301 ...(!isNullOrUndefined(sampledValueContext) && { context: sampledValueContext }),
1302 ...(!isNullOrUndefined(sampledValueTemplate.measurand) && {
1303 measurand: sampledValueTemplate.measurand,
1304 }),
1305 ...(!isNullOrUndefined(sampledValueLocation) && { location: sampledValueLocation }),
1306 ...(!isNullOrUndefined(sampledValueValue) && { value: sampledValueValue.toString() }),
1307 ...(!isNullOrUndefined(sampledValuePhase) && { phase: sampledValuePhase }),
1308 } as OCPP16SampledValue;
1309 }
1310
1311 private static checkMeasurandPowerDivider(
1312 chargingStation: ChargingStation,
1313 measurandType: OCPP16MeterValueMeasurand,
1314 ): void {
1315 if (isUndefined(chargingStation.powerDivider)) {
1316 const errMsg = `MeterValues measurand ${
1317 measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1318 }: powerDivider is undefined`;
1319 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
1320 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
1321 } else if (chargingStation?.powerDivider <= 0) {
1322 const errMsg = `MeterValues measurand ${
1323 measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1324 }: powerDivider have zero or below value ${chargingStation.powerDivider}`;
1325 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
1326 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
1327 }
1328 }
1329
1330 private static getMeasurandDefaultLocation(
1331 measurandType: OCPP16MeterValueMeasurand,
1332 ): MeterValueLocation | undefined {
1333 switch (measurandType) {
1334 case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
1335 return MeterValueLocation.EV;
1336 }
1337 }
1338
1339 // private static getMeasurandDefaultUnit(
1340 // measurandType: OCPP16MeterValueMeasurand,
1341 // ): MeterValueUnit | undefined {
1342 // switch (measurandType) {
1343 // case OCPP16MeterValueMeasurand.CURRENT_EXPORT:
1344 // case OCPP16MeterValueMeasurand.CURRENT_IMPORT:
1345 // case OCPP16MeterValueMeasurand.CURRENT_OFFERED:
1346 // return MeterValueUnit.AMP;
1347 // case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER:
1348 // case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER:
1349 // return MeterValueUnit.WATT_HOUR;
1350 // case OCPP16MeterValueMeasurand.POWER_ACTIVE_EXPORT:
1351 // case OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT:
1352 // case OCPP16MeterValueMeasurand.POWER_OFFERED:
1353 // return MeterValueUnit.WATT;
1354 // case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
1355 // return MeterValueUnit.PERCENT;
1356 // case OCPP16MeterValueMeasurand.VOLTAGE:
1357 // return MeterValueUnit.VOLT;
1358 // }
1359 // }
1360 }