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