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