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