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