refactor: factor out charging schedule composition code
[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 chargingSchedule1: OCPP16ChargingSchedule | undefined,
937 chargingSchedule2: OCPP16ChargingSchedule | undefined,
938 targetInterval: Interval,
939 ): OCPP16ChargingSchedule | undefined => {
940 if (!chargingSchedule1 && !chargingSchedule2) {
941 return undefined;
942 }
943 if (chargingSchedule1 && !chargingSchedule2) {
944 return OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule1, targetInterval);
945 }
946 if (!chargingSchedule1 && chargingSchedule2) {
947 return OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule2, targetInterval);
948 }
949 const compositeChargingSchedule1: OCPP16ChargingSchedule | undefined =
950 OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule1!, targetInterval);
951 const compositeChargingSchedule2: OCPP16ChargingSchedule | undefined =
952 OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule2!, targetInterval);
953 const compositeChargingScheduleInterval1: Interval = {
954 start: compositeChargingSchedule1!.startSchedule!,
955 end: addSeconds(
956 compositeChargingSchedule1!.startSchedule!,
957 compositeChargingSchedule1!.duration!,
958 ),
959 };
960 const compositeChargingScheduleInterval2: Interval = {
961 start: compositeChargingSchedule2!.startSchedule!,
962 end: addSeconds(
963 compositeChargingSchedule2!.startSchedule!,
964 compositeChargingSchedule2!.duration!,
965 ),
966 };
967 if (
968 !areIntervalsOverlapping(
969 compositeChargingScheduleInterval1,
970 compositeChargingScheduleInterval2,
971 )
972 ) {
973 return {
974 ...OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule1!, targetInterval)!,
975 ...OCPP16ServiceUtils.composeChargingSchedule(chargingSchedule2!, targetInterval)!,
976 };
977 }
978 // FIXME: Handle overlapping intervals
979 };
980
981 public static hasReservation = (
982 chargingStation: ChargingStation,
983 connectorId: number,
984 idTag: string,
985 ): boolean => {
986 const connectorReservation = chargingStation.getReservationBy('connectorId', connectorId);
987 const chargingStationReservation = chargingStation.getReservationBy('connectorId', 0);
988 if (
989 (chargingStation.getConnectorStatus(connectorId)?.status ===
990 OCPP16ChargePointStatus.Reserved &&
991 connectorReservation &&
992 !hasReservationExpired(connectorReservation) &&
993 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
994 connectorReservation?.idTag === idTag) ||
995 (chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved &&
996 chargingStationReservation &&
997 !hasReservationExpired(chargingStationReservation) &&
998 chargingStationReservation?.idTag === idTag)
999 ) {
1000 logger.debug(
1001 `${chargingStation.logPrefix()} Connector id ${connectorId} has a valid reservation for idTag ${idTag}: %j`,
1002 connectorReservation ?? chargingStationReservation,
1003 );
1004 return true;
1005 }
1006 return false;
1007 };
1008
1009 public static parseJsonSchemaFile<T extends JsonType>(
1010 relativePath: string,
1011 moduleName?: string,
1012 methodName?: string,
1013 ): JSONSchemaType<T> {
1014 return super.parseJsonSchemaFile<T>(
1015 relativePath,
1016 OCPPVersion.VERSION_16,
1017 moduleName,
1018 methodName,
1019 );
1020 }
1021
1022 private static composeChargingSchedule = (
1023 chargingSchedule: OCPP16ChargingSchedule,
1024 targetInterval: Interval,
1025 ): OCPP16ChargingSchedule | undefined => {
1026 const chargingScheduleInterval: Interval = {
1027 start: chargingSchedule.startSchedule!,
1028 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
1029 };
1030 if (areIntervalsOverlapping(chargingScheduleInterval, targetInterval)) {
1031 chargingSchedule.chargingSchedulePeriod.sort((a, b) => a.startPeriod - b.startPeriod);
1032 if (isBefore(chargingScheduleInterval.start, targetInterval.start)) {
1033 return {
1034 ...chargingSchedule,
1035 startSchedule: targetInterval.start as Date,
1036 duration: differenceInSeconds(chargingScheduleInterval.end, targetInterval.start as Date),
1037 chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter(
1038 (schedulePeriod, index) => {
1039 if (
1040 isWithinInterval(
1041 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
1042 targetInterval,
1043 )
1044 ) {
1045 return true;
1046 }
1047 if (
1048 index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
1049 !isWithinInterval(
1050 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod),
1051 targetInterval,
1052 ) &&
1053 isWithinInterval(
1054 addSeconds(
1055 chargingScheduleInterval.start,
1056 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
1057 ),
1058 targetInterval,
1059 )
1060 ) {
1061 schedulePeriod.startPeriod = 0;
1062 return true;
1063 }
1064 return false;
1065 },
1066 ),
1067 };
1068 }
1069 if (isAfter(chargingScheduleInterval.end, targetInterval.end)) {
1070 return {
1071 ...chargingSchedule,
1072 duration: differenceInSeconds(targetInterval.end as Date, chargingScheduleInterval.start),
1073 chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter((schedulePeriod) =>
1074 isWithinInterval(
1075 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
1076 targetInterval,
1077 ),
1078 ),
1079 };
1080 }
1081 return chargingSchedule;
1082 }
1083 };
1084
1085 private static buildSampledValue(
1086 sampledValueTemplate: SampledValueTemplate,
1087 value: number,
1088 context?: MeterValueContext,
1089 phase?: OCPP16MeterValuePhase,
1090 ): OCPP16SampledValue {
1091 const sampledValueValue = value ?? sampledValueTemplate?.value ?? null;
1092 const sampledValueContext = context ?? sampledValueTemplate?.context ?? null;
1093 const sampledValueLocation =
1094 sampledValueTemplate?.location ??
1095 OCPP16ServiceUtils.getMeasurandDefaultLocation(sampledValueTemplate.measurand!);
1096 const sampledValuePhase = phase ?? sampledValueTemplate?.phase ?? null;
1097 return {
1098 ...(!isNullOrUndefined(sampledValueTemplate.unit) && {
1099 unit: sampledValueTemplate.unit,
1100 }),
1101 ...(!isNullOrUndefined(sampledValueContext) && { context: sampledValueContext }),
1102 ...(!isNullOrUndefined(sampledValueTemplate.measurand) && {
1103 measurand: sampledValueTemplate.measurand,
1104 }),
1105 ...(!isNullOrUndefined(sampledValueLocation) && { location: sampledValueLocation }),
1106 ...(!isNullOrUndefined(sampledValueValue) && { value: sampledValueValue.toString() }),
1107 ...(!isNullOrUndefined(sampledValuePhase) && { phase: sampledValuePhase }),
1108 } as OCPP16SampledValue;
1109 }
1110
1111 private static checkMeasurandPowerDivider(
1112 chargingStation: ChargingStation,
1113 measurandType: OCPP16MeterValueMeasurand,
1114 ): void {
1115 if (isUndefined(chargingStation.powerDivider)) {
1116 const errMsg = `MeterValues measurand ${
1117 measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1118 }: powerDivider is undefined`;
1119 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
1120 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
1121 } else if (chargingStation?.powerDivider <= 0) {
1122 const errMsg = `MeterValues measurand ${
1123 measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1124 }: powerDivider have zero or below value ${chargingStation.powerDivider}`;
1125 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
1126 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
1127 }
1128 }
1129
1130 private static getMeasurandDefaultLocation(
1131 measurandType: OCPP16MeterValueMeasurand,
1132 ): MeterValueLocation | undefined {
1133 switch (measurandType) {
1134 case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
1135 return MeterValueLocation.EV;
1136 }
1137 }
1138
1139 // private static getMeasurandDefaultUnit(
1140 // measurandType: OCPP16MeterValueMeasurand,
1141 // ): MeterValueUnit | undefined {
1142 // switch (measurandType) {
1143 // case OCPP16MeterValueMeasurand.CURRENT_EXPORT:
1144 // case OCPP16MeterValueMeasurand.CURRENT_IMPORT:
1145 // case OCPP16MeterValueMeasurand.CURRENT_OFFERED:
1146 // return MeterValueUnit.AMP;
1147 // case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER:
1148 // case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER:
1149 // return MeterValueUnit.WATT_HOUR;
1150 // case OCPP16MeterValueMeasurand.POWER_ACTIVE_EXPORT:
1151 // case OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT:
1152 // case OCPP16MeterValueMeasurand.POWER_OFFERED:
1153 // return MeterValueUnit.WATT;
1154 // case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
1155 // return MeterValueUnit.PERCENT;
1156 // case OCPP16MeterValueMeasurand.VOLTAGE:
1157 // return MeterValueUnit.VOLT;
1158 // }
1159 // }
1160 }