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