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