refactor: factor out change availability helper
[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
5 import { OCPP16Constants } from './OCPP16Constants';
6 import { type ChargingStation, hasFeatureProfile } from '../../../charging-station';
7 import { OCPPError } from '../../../exception';
8 import {
9 type ClearChargingProfileRequest,
10 CurrentType,
11 ErrorType,
12 type JsonType,
13 type MeasurandPerPhaseSampledValueTemplates,
14 type MeasurandValues,
15 MeterValueContext,
16 MeterValueLocation,
17 MeterValueUnit,
18 OCPP16AvailabilityType,
19 type OCPP16ChangeAvailabilityResponse,
20 OCPP16ChargePointStatus,
21 type OCPP16ChargingProfile,
22 type OCPP16IncomingRequestCommand,
23 type OCPP16MeterValue,
24 OCPP16MeterValueMeasurand,
25 OCPP16MeterValuePhase,
26 OCPP16RequestCommand,
27 type OCPP16SampledValue,
28 OCPP16StandardParametersKey,
29 type OCPP16SupportedFeatureProfiles,
30 OCPPVersion,
31 type SampledValueTemplate,
32 Voltage,
33 } from '../../../types';
34 import {
35 ACElectricUtils,
36 Constants,
37 DCElectricUtils,
38 convertToFloat,
39 convertToInt,
40 getRandomFloatFluctuatedRounded,
41 getRandomFloatRounded,
42 getRandomInteger,
43 isNotEmptyArray,
44 isNullOrUndefined,
45 isUndefined,
46 logger,
47 roundTo,
48 } from '../../../utils';
49 import { OCPPServiceUtils } from '../OCPPServiceUtils';
50
51 export class OCPP16ServiceUtils extends OCPPServiceUtils {
52 public static checkFeatureProfile(
53 chargingStation: ChargingStation,
54 featureProfile: OCPP16SupportedFeatureProfiles,
55 command: OCPP16RequestCommand | OCPP16IncomingRequestCommand,
56 ): boolean {
57 if (!hasFeatureProfile(chargingStation, featureProfile)) {
58 logger.warn(
59 `${chargingStation.logPrefix()} Trying to '${command}' without '${featureProfile}' feature enabled in ${
60 OCPP16StandardParametersKey.SupportedFeatureProfiles
61 } in configuration`,
62 );
63 return false;
64 }
65 return true;
66 }
67
68 public static buildMeterValue(
69 chargingStation: ChargingStation,
70 connectorId: number,
71 transactionId: number,
72 interval: number,
73 debug = false,
74 ): OCPP16MeterValue {
75 const meterValue: OCPP16MeterValue = {
76 timestamp: new Date(),
77 sampledValue: [],
78 };
79 const connector = chargingStation.getConnectorStatus(connectorId);
80 // SoC measurand
81 const socSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
82 chargingStation,
83 connectorId,
84 OCPP16MeterValueMeasurand.STATE_OF_CHARGE,
85 );
86 if (socSampledValueTemplate) {
87 const socMaximumValue = 100;
88 const socMinimumValue = socSampledValueTemplate.minimumValue ?? 0;
89 const socSampledValueTemplateValue = socSampledValueTemplate.value
90 ? getRandomFloatFluctuatedRounded(
91 parseInt(socSampledValueTemplate.value),
92 socSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT,
93 )
94 : getRandomInteger(socMaximumValue, socMinimumValue);
95 meterValue.sampledValue.push(
96 OCPP16ServiceUtils.buildSampledValue(socSampledValueTemplate, socSampledValueTemplateValue),
97 );
98 const sampledValuesIndex = meterValue.sampledValue.length - 1;
99 if (
100 convertToInt(meterValue.sampledValue[sampledValuesIndex].value) > socMaximumValue ||
101 convertToInt(meterValue.sampledValue[sampledValuesIndex].value) < socMinimumValue ||
102 debug
103 ) {
104 logger.error(
105 `${chargingStation.logPrefix()} MeterValues measurand ${
106 meterValue.sampledValue[sampledValuesIndex].measurand ??
107 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
108 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${socMinimumValue}/${
109 meterValue.sampledValue[sampledValuesIndex].value
110 }/${socMaximumValue}}`,
111 );
112 }
113 }
114 // Voltage measurand
115 const voltageSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
116 chargingStation,
117 connectorId,
118 OCPP16MeterValueMeasurand.VOLTAGE,
119 );
120 if (voltageSampledValueTemplate) {
121 const voltageSampledValueTemplateValue = voltageSampledValueTemplate.value
122 ? parseInt(voltageSampledValueTemplate.value)
123 : chargingStation.getVoltageOut();
124 const fluctuationPercent =
125 voltageSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT;
126 const voltageMeasurandValue = getRandomFloatFluctuatedRounded(
127 voltageSampledValueTemplateValue,
128 fluctuationPercent,
129 );
130 if (
131 chargingStation.getNumberOfPhases() !== 3 ||
132 (chargingStation.getNumberOfPhases() === 3 && chargingStation.getMainVoltageMeterValues())
133 ) {
134 meterValue.sampledValue.push(
135 OCPP16ServiceUtils.buildSampledValue(voltageSampledValueTemplate, voltageMeasurandValue),
136 );
137 }
138 for (
139 let phase = 1;
140 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
141 phase++
142 ) {
143 const phaseLineToNeutralValue = `L${phase}-N`;
144 const voltagePhaseLineToNeutralSampledValueTemplate =
145 OCPP16ServiceUtils.getSampledValueTemplate(
146 chargingStation,
147 connectorId,
148 OCPP16MeterValueMeasurand.VOLTAGE,
149 phaseLineToNeutralValue as OCPP16MeterValuePhase,
150 );
151 let voltagePhaseLineToNeutralMeasurandValue: number | undefined;
152 if (voltagePhaseLineToNeutralSampledValueTemplate) {
153 const voltagePhaseLineToNeutralSampledValueTemplateValue =
154 voltagePhaseLineToNeutralSampledValueTemplate.value
155 ? parseInt(voltagePhaseLineToNeutralSampledValueTemplate.value)
156 : chargingStation.getVoltageOut();
157 const fluctuationPhaseToNeutralPercent =
158 voltagePhaseLineToNeutralSampledValueTemplate.fluctuationPercent ??
159 Constants.DEFAULT_FLUCTUATION_PERCENT;
160 voltagePhaseLineToNeutralMeasurandValue = getRandomFloatFluctuatedRounded(
161 voltagePhaseLineToNeutralSampledValueTemplateValue,
162 fluctuationPhaseToNeutralPercent,
163 );
164 }
165 meterValue.sampledValue.push(
166 OCPP16ServiceUtils.buildSampledValue(
167 voltagePhaseLineToNeutralSampledValueTemplate ?? voltageSampledValueTemplate,
168 voltagePhaseLineToNeutralMeasurandValue ?? voltageMeasurandValue,
169 undefined,
170 phaseLineToNeutralValue as OCPP16MeterValuePhase,
171 ),
172 );
173 if (chargingStation.getPhaseLineToLineVoltageMeterValues()) {
174 const phaseLineToLineValue = `L${phase}-L${
175 (phase + 1) % chargingStation.getNumberOfPhases() !== 0
176 ? (phase + 1) % chargingStation.getNumberOfPhases()
177 : chargingStation.getNumberOfPhases()
178 }`;
179 const voltagePhaseLineToLineSampledValueTemplate =
180 OCPP16ServiceUtils.getSampledValueTemplate(
181 chargingStation,
182 connectorId,
183 OCPP16MeterValueMeasurand.VOLTAGE,
184 phaseLineToLineValue as OCPP16MeterValuePhase,
185 );
186 let voltagePhaseLineToLineMeasurandValue: number | undefined;
187 if (voltagePhaseLineToLineSampledValueTemplate) {
188 const voltagePhaseLineToLineSampledValueTemplateValue =
189 voltagePhaseLineToLineSampledValueTemplate.value
190 ? parseInt(voltagePhaseLineToLineSampledValueTemplate.value)
191 : Voltage.VOLTAGE_400;
192 const fluctuationPhaseLineToLinePercent =
193 voltagePhaseLineToLineSampledValueTemplate.fluctuationPercent ??
194 Constants.DEFAULT_FLUCTUATION_PERCENT;
195 voltagePhaseLineToLineMeasurandValue = getRandomFloatFluctuatedRounded(
196 voltagePhaseLineToLineSampledValueTemplateValue,
197 fluctuationPhaseLineToLinePercent,
198 );
199 }
200 const defaultVoltagePhaseLineToLineMeasurandValue = getRandomFloatFluctuatedRounded(
201 Voltage.VOLTAGE_400,
202 fluctuationPercent,
203 );
204 meterValue.sampledValue.push(
205 OCPP16ServiceUtils.buildSampledValue(
206 voltagePhaseLineToLineSampledValueTemplate ?? voltageSampledValueTemplate,
207 voltagePhaseLineToLineMeasurandValue ?? defaultVoltagePhaseLineToLineMeasurandValue,
208 undefined,
209 phaseLineToLineValue as OCPP16MeterValuePhase,
210 ),
211 );
212 }
213 }
214 }
215 // Power.Active.Import measurand
216 const powerSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
217 chargingStation,
218 connectorId,
219 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
220 );
221 let powerPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {};
222 if (chargingStation.getNumberOfPhases() === 3) {
223 powerPerPhaseSampledValueTemplates = {
224 L1: OCPP16ServiceUtils.getSampledValueTemplate(
225 chargingStation,
226 connectorId,
227 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
228 OCPP16MeterValuePhase.L1_N,
229 ),
230 L2: OCPP16ServiceUtils.getSampledValueTemplate(
231 chargingStation,
232 connectorId,
233 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
234 OCPP16MeterValuePhase.L2_N,
235 ),
236 L3: OCPP16ServiceUtils.getSampledValueTemplate(
237 chargingStation,
238 connectorId,
239 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
240 OCPP16MeterValuePhase.L3_N,
241 ),
242 };
243 }
244 if (powerSampledValueTemplate) {
245 OCPP16ServiceUtils.checkMeasurandPowerDivider(
246 chargingStation,
247 powerSampledValueTemplate.measurand!,
248 );
249 const errMsg = `MeterValues measurand ${
250 powerSampledValueTemplate.measurand ??
251 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
252 }: Unknown ${chargingStation.getCurrentOutType()} currentOutType in template file ${
253 chargingStation.templateFile
254 }, cannot calculate ${
255 powerSampledValueTemplate.measurand ??
256 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
257 } measurand value`;
258 const powerMeasurandValues: MeasurandValues = {} as MeasurandValues;
259 const unitDivider = powerSampledValueTemplate?.unit === MeterValueUnit.KILO_WATT ? 1000 : 1;
260 const connectorMaximumAvailablePower =
261 chargingStation.getConnectorMaximumAvailablePower(connectorId);
262 const connectorMaximumPower = Math.round(connectorMaximumAvailablePower);
263 const connectorMaximumPowerPerPhase = Math.round(
264 connectorMaximumAvailablePower / chargingStation.getNumberOfPhases(),
265 );
266 const connectorMinimumPower = Math.round(powerSampledValueTemplate.minimumValue!) ?? 0;
267 const connectorMinimumPowerPerPhase = Math.round(
268 connectorMinimumPower / chargingStation.getNumberOfPhases(),
269 );
270 switch (chargingStation.getCurrentOutType()) {
271 case CurrentType.AC:
272 if (chargingStation.getNumberOfPhases() === 3) {
273 const defaultFluctuatedPowerPerPhase =
274 powerSampledValueTemplate.value &&
275 getRandomFloatFluctuatedRounded(
276 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
277 powerSampledValueTemplate.value,
278 connectorMaximumPower / unitDivider,
279 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() },
280 ) / chargingStation.getNumberOfPhases(),
281 powerSampledValueTemplate.fluctuationPercent ??
282 Constants.DEFAULT_FLUCTUATION_PERCENT,
283 );
284 const phase1FluctuatedValue =
285 powerPerPhaseSampledValueTemplates.L1?.value &&
286 getRandomFloatFluctuatedRounded(
287 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
288 powerPerPhaseSampledValueTemplates.L1.value,
289 connectorMaximumPowerPerPhase / unitDivider,
290 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() },
291 ),
292 powerPerPhaseSampledValueTemplates.L1.fluctuationPercent ??
293 Constants.DEFAULT_FLUCTUATION_PERCENT,
294 );
295 const phase2FluctuatedValue =
296 powerPerPhaseSampledValueTemplates.L2?.value &&
297 getRandomFloatFluctuatedRounded(
298 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
299 powerPerPhaseSampledValueTemplates.L2.value,
300 connectorMaximumPowerPerPhase / unitDivider,
301 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() },
302 ),
303 powerPerPhaseSampledValueTemplates.L2.fluctuationPercent ??
304 Constants.DEFAULT_FLUCTUATION_PERCENT,
305 );
306 const phase3FluctuatedValue =
307 powerPerPhaseSampledValueTemplates.L3?.value &&
308 getRandomFloatFluctuatedRounded(
309 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
310 powerPerPhaseSampledValueTemplates.L3.value,
311 connectorMaximumPowerPerPhase / unitDivider,
312 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() },
313 ),
314 powerPerPhaseSampledValueTemplates.L3.fluctuationPercent ??
315 Constants.DEFAULT_FLUCTUATION_PERCENT,
316 );
317 powerMeasurandValues.L1 =
318 (phase1FluctuatedValue as number) ??
319 (defaultFluctuatedPowerPerPhase as number) ??
320 getRandomFloatRounded(
321 connectorMaximumPowerPerPhase / unitDivider,
322 connectorMinimumPowerPerPhase / unitDivider,
323 );
324 powerMeasurandValues.L2 =
325 (phase2FluctuatedValue as number) ??
326 (defaultFluctuatedPowerPerPhase as number) ??
327 getRandomFloatRounded(
328 connectorMaximumPowerPerPhase / unitDivider,
329 connectorMinimumPowerPerPhase / unitDivider,
330 );
331 powerMeasurandValues.L3 =
332 (phase3FluctuatedValue as number) ??
333 (defaultFluctuatedPowerPerPhase as number) ??
334 getRandomFloatRounded(
335 connectorMaximumPowerPerPhase / unitDivider,
336 connectorMinimumPowerPerPhase / unitDivider,
337 );
338 } else {
339 powerMeasurandValues.L1 = powerSampledValueTemplate.value
340 ? getRandomFloatFluctuatedRounded(
341 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
342 powerSampledValueTemplate.value,
343 connectorMaximumPower / unitDivider,
344 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() },
345 ),
346 powerSampledValueTemplate.fluctuationPercent ??
347 Constants.DEFAULT_FLUCTUATION_PERCENT,
348 )
349 : getRandomFloatRounded(
350 connectorMaximumPower / unitDivider,
351 connectorMinimumPower / unitDivider,
352 );
353 powerMeasurandValues.L2 = 0;
354 powerMeasurandValues.L3 = 0;
355 }
356 powerMeasurandValues.allPhases = roundTo(
357 powerMeasurandValues.L1 + powerMeasurandValues.L2 + powerMeasurandValues.L3,
358 2,
359 );
360 break;
361 case CurrentType.DC:
362 powerMeasurandValues.allPhases = powerSampledValueTemplate.value
363 ? getRandomFloatFluctuatedRounded(
364 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
365 powerSampledValueTemplate.value,
366 connectorMaximumPower / unitDivider,
367 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() },
368 ),
369 powerSampledValueTemplate.fluctuationPercent ??
370 Constants.DEFAULT_FLUCTUATION_PERCENT,
371 )
372 : getRandomFloatRounded(
373 connectorMaximumPower / unitDivider,
374 connectorMinimumPower / unitDivider,
375 );
376 break;
377 default:
378 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
379 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
380 }
381 meterValue.sampledValue.push(
382 OCPP16ServiceUtils.buildSampledValue(
383 powerSampledValueTemplate,
384 powerMeasurandValues.allPhases,
385 ),
386 );
387 const sampledValuesIndex = meterValue.sampledValue.length - 1;
388 const connectorMaximumPowerRounded = roundTo(connectorMaximumPower / unitDivider, 2);
389 const connectorMinimumPowerRounded = roundTo(connectorMinimumPower / unitDivider, 2);
390 if (
391 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
392 connectorMaximumPowerRounded ||
393 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) <
394 connectorMinimumPowerRounded ||
395 debug
396 ) {
397 logger.error(
398 `${chargingStation.logPrefix()} MeterValues measurand ${
399 meterValue.sampledValue[sampledValuesIndex].measurand ??
400 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
401 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumPowerRounded}/${
402 meterValue.sampledValue[sampledValuesIndex].value
403 }/${connectorMaximumPowerRounded}`,
404 );
405 }
406 for (
407 let phase = 1;
408 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
409 phase++
410 ) {
411 const phaseValue = `L${phase}-N`;
412 meterValue.sampledValue.push(
413 OCPP16ServiceUtils.buildSampledValue(
414 powerPerPhaseSampledValueTemplates[
415 `L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates
416 ]! ?? powerSampledValueTemplate,
417 powerMeasurandValues[`L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates],
418 undefined,
419 phaseValue as OCPP16MeterValuePhase,
420 ),
421 );
422 const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1;
423 const connectorMaximumPowerPerPhaseRounded = roundTo(
424 connectorMaximumPowerPerPhase / unitDivider,
425 2,
426 );
427 const connectorMinimumPowerPerPhaseRounded = roundTo(
428 connectorMinimumPowerPerPhase / unitDivider,
429 2,
430 );
431 if (
432 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
433 connectorMaximumPowerPerPhaseRounded ||
434 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
435 connectorMinimumPowerPerPhaseRounded ||
436 debug
437 ) {
438 logger.error(
439 `${chargingStation.logPrefix()} MeterValues measurand ${
440 meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ??
441 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
442 }: phase ${
443 meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
444 }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumPowerPerPhaseRounded}/${
445 meterValue.sampledValue[sampledValuesPerPhaseIndex].value
446 }/${connectorMaximumPowerPerPhaseRounded}`,
447 );
448 }
449 }
450 }
451 // Current.Import measurand
452 const currentSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
453 chargingStation,
454 connectorId,
455 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
456 );
457 let currentPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {};
458 if (chargingStation.getNumberOfPhases() === 3) {
459 currentPerPhaseSampledValueTemplates = {
460 L1: OCPP16ServiceUtils.getSampledValueTemplate(
461 chargingStation,
462 connectorId,
463 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
464 OCPP16MeterValuePhase.L1,
465 ),
466 L2: OCPP16ServiceUtils.getSampledValueTemplate(
467 chargingStation,
468 connectorId,
469 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
470 OCPP16MeterValuePhase.L2,
471 ),
472 L3: OCPP16ServiceUtils.getSampledValueTemplate(
473 chargingStation,
474 connectorId,
475 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
476 OCPP16MeterValuePhase.L3,
477 ),
478 };
479 }
480 if (currentSampledValueTemplate) {
481 OCPP16ServiceUtils.checkMeasurandPowerDivider(
482 chargingStation,
483 currentSampledValueTemplate.measurand!,
484 );
485 const errMsg = `MeterValues measurand ${
486 currentSampledValueTemplate.measurand ??
487 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
488 }: Unknown ${chargingStation.getCurrentOutType()} currentOutType in template file ${
489 chargingStation.templateFile
490 }, cannot calculate ${
491 currentSampledValueTemplate.measurand ??
492 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
493 } measurand value`;
494 const currentMeasurandValues: MeasurandValues = {} as MeasurandValues;
495 const connectorMaximumAvailablePower =
496 chargingStation.getConnectorMaximumAvailablePower(connectorId);
497 const connectorMinimumAmperage = currentSampledValueTemplate.minimumValue ?? 0;
498 let connectorMaximumAmperage: number;
499 switch (chargingStation.getCurrentOutType()) {
500 case CurrentType.AC:
501 connectorMaximumAmperage = ACElectricUtils.amperagePerPhaseFromPower(
502 chargingStation.getNumberOfPhases(),
503 connectorMaximumAvailablePower,
504 chargingStation.getVoltageOut(),
505 );
506 if (chargingStation.getNumberOfPhases() === 3) {
507 const defaultFluctuatedAmperagePerPhase =
508 currentSampledValueTemplate.value &&
509 getRandomFloatFluctuatedRounded(
510 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
511 currentSampledValueTemplate.value,
512 connectorMaximumAmperage,
513 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() },
514 ),
515 currentSampledValueTemplate.fluctuationPercent ??
516 Constants.DEFAULT_FLUCTUATION_PERCENT,
517 );
518 const phase1FluctuatedValue =
519 currentPerPhaseSampledValueTemplates.L1?.value &&
520 getRandomFloatFluctuatedRounded(
521 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
522 currentPerPhaseSampledValueTemplates.L1.value,
523 connectorMaximumAmperage,
524 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() },
525 ),
526 currentPerPhaseSampledValueTemplates.L1.fluctuationPercent ??
527 Constants.DEFAULT_FLUCTUATION_PERCENT,
528 );
529 const phase2FluctuatedValue =
530 currentPerPhaseSampledValueTemplates.L2?.value &&
531 getRandomFloatFluctuatedRounded(
532 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
533 currentPerPhaseSampledValueTemplates.L2.value,
534 connectorMaximumAmperage,
535 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() },
536 ),
537 currentPerPhaseSampledValueTemplates.L2.fluctuationPercent ??
538 Constants.DEFAULT_FLUCTUATION_PERCENT,
539 );
540 const phase3FluctuatedValue =
541 currentPerPhaseSampledValueTemplates.L3?.value &&
542 getRandomFloatFluctuatedRounded(
543 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
544 currentPerPhaseSampledValueTemplates.L3.value,
545 connectorMaximumAmperage,
546 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() },
547 ),
548 currentPerPhaseSampledValueTemplates.L3.fluctuationPercent ??
549 Constants.DEFAULT_FLUCTUATION_PERCENT,
550 );
551 currentMeasurandValues.L1 =
552 (phase1FluctuatedValue as number) ??
553 (defaultFluctuatedAmperagePerPhase as number) ??
554 getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
555 currentMeasurandValues.L2 =
556 (phase2FluctuatedValue as number) ??
557 (defaultFluctuatedAmperagePerPhase as number) ??
558 getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
559 currentMeasurandValues.L3 =
560 (phase3FluctuatedValue as number) ??
561 (defaultFluctuatedAmperagePerPhase as number) ??
562 getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
563 } else {
564 currentMeasurandValues.L1 = currentSampledValueTemplate.value
565 ? getRandomFloatFluctuatedRounded(
566 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
567 currentSampledValueTemplate.value,
568 connectorMaximumAmperage,
569 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() },
570 ),
571 currentSampledValueTemplate.fluctuationPercent ??
572 Constants.DEFAULT_FLUCTUATION_PERCENT,
573 )
574 : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
575 currentMeasurandValues.L2 = 0;
576 currentMeasurandValues.L3 = 0;
577 }
578 currentMeasurandValues.allPhases = roundTo(
579 (currentMeasurandValues.L1 + currentMeasurandValues.L2 + currentMeasurandValues.L3) /
580 chargingStation.getNumberOfPhases(),
581 2,
582 );
583 break;
584 case CurrentType.DC:
585 connectorMaximumAmperage = DCElectricUtils.amperage(
586 connectorMaximumAvailablePower,
587 chargingStation.getVoltageOut(),
588 );
589 currentMeasurandValues.allPhases = currentSampledValueTemplate.value
590 ? getRandomFloatFluctuatedRounded(
591 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
592 currentSampledValueTemplate.value,
593 connectorMaximumAmperage,
594 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() },
595 ),
596 currentSampledValueTemplate.fluctuationPercent ??
597 Constants.DEFAULT_FLUCTUATION_PERCENT,
598 )
599 : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
600 break;
601 default:
602 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
603 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
604 }
605 meterValue.sampledValue.push(
606 OCPP16ServiceUtils.buildSampledValue(
607 currentSampledValueTemplate,
608 currentMeasurandValues.allPhases,
609 ),
610 );
611 const sampledValuesIndex = meterValue.sampledValue.length - 1;
612 if (
613 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
614 connectorMaximumAmperage ||
615 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) <
616 connectorMinimumAmperage ||
617 debug
618 ) {
619 logger.error(
620 `${chargingStation.logPrefix()} MeterValues measurand ${
621 meterValue.sampledValue[sampledValuesIndex].measurand ??
622 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
623 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${
624 meterValue.sampledValue[sampledValuesIndex].value
625 }/${connectorMaximumAmperage}`,
626 );
627 }
628 for (
629 let phase = 1;
630 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
631 phase++
632 ) {
633 const phaseValue = `L${phase}`;
634 meterValue.sampledValue.push(
635 OCPP16ServiceUtils.buildSampledValue(
636 currentPerPhaseSampledValueTemplates[
637 phaseValue as keyof MeasurandPerPhaseSampledValueTemplates
638 ]! ?? currentSampledValueTemplate,
639 currentMeasurandValues[phaseValue as keyof MeasurandPerPhaseSampledValueTemplates],
640 undefined,
641 phaseValue as OCPP16MeterValuePhase,
642 ),
643 );
644 const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1;
645 if (
646 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
647 connectorMaximumAmperage ||
648 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
649 connectorMinimumAmperage ||
650 debug
651 ) {
652 logger.error(
653 `${chargingStation.logPrefix()} MeterValues measurand ${
654 meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ??
655 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
656 }: phase ${
657 meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
658 }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${
659 meterValue.sampledValue[sampledValuesPerPhaseIndex].value
660 }/${connectorMaximumAmperage}`,
661 );
662 }
663 }
664 }
665 // Energy.Active.Import.Register measurand (default)
666 const energySampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
667 chargingStation,
668 connectorId,
669 );
670 if (energySampledValueTemplate) {
671 OCPP16ServiceUtils.checkMeasurandPowerDivider(
672 chargingStation,
673 energySampledValueTemplate.measurand!,
674 );
675 const unitDivider =
676 energySampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
677 const connectorMaximumAvailablePower =
678 chargingStation.getConnectorMaximumAvailablePower(connectorId);
679 const connectorMaximumEnergyRounded = roundTo(
680 (connectorMaximumAvailablePower * interval) / (3600 * 1000),
681 2,
682 );
683 const energyValueRounded = energySampledValueTemplate.value
684 ? // Cumulate the fluctuated value around the static one
685 getRandomFloatFluctuatedRounded(
686 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
687 energySampledValueTemplate.value,
688 connectorMaximumEnergyRounded,
689 {
690 limitationEnabled: chargingStation.getCustomValueLimitationMeterValues(),
691 unitMultiplier: unitDivider,
692 },
693 ),
694 energySampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT,
695 )
696 : getRandomFloatRounded(connectorMaximumEnergyRounded);
697 // Persist previous value on connector
698 if (connector) {
699 if (
700 isNullOrUndefined(connector.energyActiveImportRegisterValue) === false &&
701 connector.energyActiveImportRegisterValue! >= 0 &&
702 isNullOrUndefined(connector.transactionEnergyActiveImportRegisterValue) === false &&
703 connector.transactionEnergyActiveImportRegisterValue! >= 0
704 ) {
705 connector.energyActiveImportRegisterValue! += energyValueRounded;
706 connector.transactionEnergyActiveImportRegisterValue! += energyValueRounded;
707 } else {
708 connector.energyActiveImportRegisterValue = 0;
709 connector.transactionEnergyActiveImportRegisterValue = 0;
710 }
711 }
712 meterValue.sampledValue.push(
713 OCPP16ServiceUtils.buildSampledValue(
714 energySampledValueTemplate,
715 roundTo(
716 chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) /
717 unitDivider,
718 2,
719 ),
720 ),
721 );
722 const sampledValuesIndex = meterValue.sampledValue.length - 1;
723 if (energyValueRounded > connectorMaximumEnergyRounded || debug) {
724 logger.error(
725 `${chargingStation.logPrefix()} MeterValues measurand ${
726 meterValue.sampledValue[sampledValuesIndex].measurand ??
727 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
728 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${interval}ms`,
729 );
730 }
731 }
732 return meterValue;
733 }
734
735 public static buildTransactionBeginMeterValue(
736 chargingStation: ChargingStation,
737 connectorId: number,
738 meterStart: number,
739 ): OCPP16MeterValue {
740 const meterValue: OCPP16MeterValue = {
741 timestamp: new Date(),
742 sampledValue: [],
743 };
744 // Energy.Active.Import.Register measurand (default)
745 const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
746 chargingStation,
747 connectorId,
748 );
749 const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
750 meterValue.sampledValue.push(
751 OCPP16ServiceUtils.buildSampledValue(
752 sampledValueTemplate!,
753 roundTo((meterStart ?? 0) / unitDivider, 4),
754 MeterValueContext.TRANSACTION_BEGIN,
755 ),
756 );
757 return meterValue;
758 }
759
760 public static buildTransactionEndMeterValue(
761 chargingStation: ChargingStation,
762 connectorId: number,
763 meterStop: number,
764 ): OCPP16MeterValue {
765 const meterValue: OCPP16MeterValue = {
766 timestamp: new Date(),
767 sampledValue: [],
768 };
769 // Energy.Active.Import.Register measurand (default)
770 const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
771 chargingStation,
772 connectorId,
773 );
774 const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
775 meterValue.sampledValue.push(
776 OCPP16ServiceUtils.buildSampledValue(
777 sampledValueTemplate!,
778 roundTo((meterStop ?? 0) / unitDivider, 4),
779 MeterValueContext.TRANSACTION_END,
780 ),
781 );
782 return meterValue;
783 }
784
785 public static buildTransactionDataMeterValues(
786 transactionBeginMeterValue: OCPP16MeterValue,
787 transactionEndMeterValue: OCPP16MeterValue,
788 ): OCPP16MeterValue[] {
789 const meterValues: OCPP16MeterValue[] = [];
790 meterValues.push(transactionBeginMeterValue);
791 meterValues.push(transactionEndMeterValue);
792 return meterValues;
793 }
794
795 public static changeAvailability = async (
796 chargingStation: ChargingStation,
797 connectorId: number,
798 chargePointStatus: OCPP16ChargePointStatus,
799 availabilityType: OCPP16AvailabilityType,
800 ): Promise<OCPP16ChangeAvailabilityResponse> => {
801 let response: OCPP16ChangeAvailabilityResponse =
802 OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
803 const connectorStatus = chargingStation.getConnectorStatus(connectorId)!;
804 if (connectorStatus?.transactionStarted === true) {
805 response = OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
806 }
807 connectorStatus.availability = availabilityType;
808 if (response === OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED) {
809 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
810 chargingStation,
811 connectorId,
812 chargePointStatus,
813 );
814 }
815 return response;
816 };
817
818 public static setChargingProfile(
819 chargingStation: ChargingStation,
820 connectorId: number,
821 cp: OCPP16ChargingProfile,
822 ): void {
823 if (isNullOrUndefined(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
824 logger.error(
825 `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an uninitialized charging profiles array attribute, applying deferred initialization`,
826 );
827 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = [];
828 }
829 if (
830 Array.isArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles) === false
831 ) {
832 logger.error(
833 `${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 initialization`,
834 );
835 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = [];
836 }
837 let cpReplaced = false;
838 if (isNotEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
839 chargingStation
840 .getConnectorStatus(connectorId)
841 ?.chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
842 if (
843 chargingProfile.chargingProfileId === cp.chargingProfileId ||
844 (chargingProfile.stackLevel === cp.stackLevel &&
845 chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)
846 ) {
847 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles![index] = cp;
848 cpReplaced = true;
849 }
850 });
851 }
852 !cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp);
853 }
854
855 public static clearChargingProfiles = (
856 chargingStation: ChargingStation,
857 commandPayload: ClearChargingProfileRequest,
858 chargingProfiles: OCPP16ChargingProfile[] | undefined,
859 ): boolean => {
860 let clearedCP = false;
861 if (isNotEmptyArray(chargingProfiles)) {
862 chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
863 let clearCurrentCP = false;
864 if (chargingProfile.chargingProfileId === commandPayload.id) {
865 clearCurrentCP = true;
866 }
867 if (
868 !commandPayload.chargingProfilePurpose &&
869 chargingProfile.stackLevel === commandPayload.stackLevel
870 ) {
871 clearCurrentCP = true;
872 }
873 if (
874 !chargingProfile.stackLevel &&
875 chargingProfile.chargingProfilePurpose === commandPayload.chargingProfilePurpose
876 ) {
877 clearCurrentCP = true;
878 }
879 if (
880 chargingProfile.stackLevel === commandPayload.stackLevel &&
881 chargingProfile.chargingProfilePurpose === commandPayload.chargingProfilePurpose
882 ) {
883 clearCurrentCP = true;
884 }
885 if (clearCurrentCP) {
886 chargingProfiles.splice(index, 1);
887 logger.debug(
888 `${chargingStation.logPrefix()} Matching charging profile(s) cleared: %j`,
889 chargingProfile,
890 );
891 clearedCP = true;
892 }
893 });
894 }
895 return clearedCP;
896 };
897
898 public static parseJsonSchemaFile<T extends JsonType>(
899 relativePath: string,
900 moduleName?: string,
901 methodName?: string,
902 ): JSONSchemaType<T> {
903 return super.parseJsonSchemaFile<T>(
904 relativePath,
905 OCPPVersion.VERSION_16,
906 moduleName,
907 methodName,
908 );
909 }
910
911 private static buildSampledValue(
912 sampledValueTemplate: SampledValueTemplate,
913 value: number,
914 context?: MeterValueContext,
915 phase?: OCPP16MeterValuePhase,
916 ): OCPP16SampledValue {
917 const sampledValueValue = value ?? sampledValueTemplate?.value ?? null;
918 const sampledValueContext = context ?? sampledValueTemplate?.context ?? null;
919 const sampledValueLocation =
920 sampledValueTemplate?.location ??
921 OCPP16ServiceUtils.getMeasurandDefaultLocation(sampledValueTemplate.measurand!);
922 const sampledValuePhase = phase ?? sampledValueTemplate?.phase ?? null;
923 return {
924 ...(!isNullOrUndefined(sampledValueTemplate.unit) && {
925 unit: sampledValueTemplate.unit,
926 }),
927 ...(!isNullOrUndefined(sampledValueContext) && { context: sampledValueContext }),
928 ...(!isNullOrUndefined(sampledValueTemplate.measurand) && {
929 measurand: sampledValueTemplate.measurand,
930 }),
931 ...(!isNullOrUndefined(sampledValueLocation) && { location: sampledValueLocation }),
932 ...(!isNullOrUndefined(sampledValueValue) && { value: sampledValueValue.toString() }),
933 ...(!isNullOrUndefined(sampledValuePhase) && { phase: sampledValuePhase }),
934 } as OCPP16SampledValue;
935 }
936
937 private static checkMeasurandPowerDivider(
938 chargingStation: ChargingStation,
939 measurandType: OCPP16MeterValueMeasurand,
940 ): void {
941 if (isUndefined(chargingStation.powerDivider)) {
942 const errMsg = `MeterValues measurand ${
943 measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
944 }: powerDivider is undefined`;
945 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
946 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
947 } else if (chargingStation?.powerDivider <= 0) {
948 const errMsg = `MeterValues measurand ${
949 measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
950 }: powerDivider have zero or below value ${chargingStation.powerDivider}`;
951 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
952 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
953 }
954 }
955
956 private static getMeasurandDefaultLocation(
957 measurandType: OCPP16MeterValueMeasurand,
958 ): MeterValueLocation | undefined {
959 switch (measurandType) {
960 case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
961 return MeterValueLocation.EV;
962 }
963 }
964
965 private static getMeasurandDefaultUnit(
966 measurandType: OCPP16MeterValueMeasurand,
967 ): MeterValueUnit | undefined {
968 switch (measurandType) {
969 case OCPP16MeterValueMeasurand.CURRENT_EXPORT:
970 case OCPP16MeterValueMeasurand.CURRENT_IMPORT:
971 case OCPP16MeterValueMeasurand.CURRENT_OFFERED:
972 return MeterValueUnit.AMP;
973 case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER:
974 case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER:
975 return MeterValueUnit.WATT_HOUR;
976 case OCPP16MeterValueMeasurand.POWER_ACTIVE_EXPORT:
977 case OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT:
978 case OCPP16MeterValueMeasurand.POWER_OFFERED:
979 return MeterValueUnit.WATT;
980 case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
981 return MeterValueUnit.PERCENT;
982 case OCPP16MeterValueMeasurand.VOLTAGE:
983 return MeterValueUnit.VOLT;
984 }
985 }
986 }