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