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