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