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