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