refactor(simulator): constify and factor out empty data structure
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16ServiceUtils.ts
CommitLineData
edd13439 1// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
c8eeb62b 2
130783a7
JB
3import path from 'node:path';
4import { fileURLToPath } from 'node:url';
5
6import type { JSONSchemaType } from 'ajv';
7
2896e06d 8import type { ChargingStation } from '../../../charging-station';
268a74bb 9import { OCPPError } from '../../../exception';
e7aeea18 10import {
268a74bb
JB
11 CurrentType,
12 ErrorType,
13 type JsonType,
14 type MeasurandPerPhaseSampledValueTemplates,
15 type MeasurandValues,
e7aeea18
JB
16 MeterValueContext,
17 MeterValueLocation,
18 MeterValueUnit,
268a74bb
JB
19 type OCPP16ChargingProfile,
20 type OCPP16IncomingRequestCommand,
27782dbc 21 type OCPP16MeterValue,
e7aeea18
JB
22 OCPP16MeterValueMeasurand,
23 OCPP16MeterValuePhase,
370ae4ee 24 OCPP16RequestCommand,
268a74bb
JB
25 type OCPP16SampledValue,
26 OCPP16StandardParametersKey,
27 type OCPP16SupportedFeatureProfiles,
28 OCPPVersion,
29 type SampledValueTemplate,
30 Voltage,
31} from '../../../types';
60a74391 32import { ACElectricUtils, Constants, DCElectricUtils, Utils, logger } from '../../../utils';
2896e06d 33import { OCPPServiceUtils } from '../internal';
6ed92bc1 34
7bc31f9c 35export class OCPP16ServiceUtils extends OCPPServiceUtils {
370ae4ee
JB
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
78085c42
JB
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 = {
c38f0ced 60 timestamp: new Date(),
78085c42
JB
61 sampledValue: [],
62 };
63 const connector = chargingStation.getConnectorStatus(connectorId);
64 // SoC measurand
ed3d2808 65 const socSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 66 chargingStation,
78085c42
JB
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
72092cfc 86 }: connectorId ${connectorId}, transaction ${connector?.transactionId}, value: ${
78085c42
JB
87 meterValue.sampledValue[sampledValuesIndex].value
88 }/100`
89 );
90 }
91 }
92 // Voltage measurand
ed3d2808 93 const voltageSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 94 chargingStation,
78085c42
JB
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 =
ed3d2808 123 OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 124 chargingStation,
78085c42
JB
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,
72092cfc 147 undefined,
78085c42
JB
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 =
ed3d2808 158 OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 159 chargingStation,
78085c42
JB
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,
72092cfc 186 undefined,
78085c42
JB
187 phaseLineToLineValue as OCPP16MeterValuePhase
188 )
189 );
190 }
191 }
192 }
193 // Power.Active.Import measurand
ed3d2808 194 const powerSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 195 chargingStation,
78085c42
JB
196 connectorId,
197 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT
198 );
59b6ed8d
JB
199 let powerPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates =
200 Constants.EMPTY_OBJECT;
78085c42
JB
201 if (chargingStation.getNumberOfPhases() === 3) {
202 powerPerPhaseSampledValueTemplates = {
ed3d2808 203 L1: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 204 chargingStation,
78085c42
JB
205 connectorId,
206 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
207 OCPP16MeterValuePhase.L1_N
208 ),
ed3d2808 209 L2: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 210 chargingStation,
78085c42
JB
211 connectorId,
212 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
213 OCPP16MeterValuePhase.L2_N
214 ),
ed3d2808 215 L3: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 216 chargingStation,
78085c42
JB
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 );
fc040c43 228 const errMsg = `MeterValues measurand ${
78085c42
JB
229 powerSampledValueTemplate.measurand ??
230 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
231 }: Unknown ${chargingStation.getCurrentOutType()} currentOutType in template file ${
2484ac1e 232 chargingStation.templateFile
78085c42
JB
233 }, cannot calculate ${
234 powerSampledValueTemplate.measurand ??
235 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
236 } measurand value`;
59b6ed8d 237 const powerMeasurandValues = Constants.EMPTY_OBJECT as MeasurandValues;
78085c42 238 const unitDivider = powerSampledValueTemplate?.unit === MeterValueUnit.KILO_WATT ? 1000 : 1;
1b6498ba
JB
239 const connectorMaximumAvailablePower =
240 chargingStation.getConnectorMaximumAvailablePower(connectorId);
241 const connectorMaximumPower = Math.round(connectorMaximumAvailablePower);
ad8537a7 242 const connectorMaximumPowerPerPhase = Math.round(
1b6498ba 243 connectorMaximumAvailablePower / chargingStation.getNumberOfPhases()
78085c42
JB
244 );
245 switch (chargingStation.getCurrentOutType()) {
246 case CurrentType.AC:
247 if (chargingStation.getNumberOfPhases() === 3) {
248 const defaultFluctuatedPowerPerPhase =
249 powerSampledValueTemplate.value &&
250 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
251 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
252 powerSampledValueTemplate.value,
253 connectorMaximumPower / unitDivider,
254 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 255 ) / chargingStation.getNumberOfPhases(),
78085c42
JB
256 powerSampledValueTemplate.fluctuationPercent ??
257 Constants.DEFAULT_FLUCTUATION_PERCENT
258 );
259 const phase1FluctuatedValue =
260 powerPerPhaseSampledValueTemplates?.L1?.value &&
261 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
262 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
263 powerPerPhaseSampledValueTemplates.L1.value,
264 connectorMaximumPowerPerPhase / unitDivider,
265 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 266 ),
78085c42
JB
267 powerPerPhaseSampledValueTemplates.L1.fluctuationPercent ??
268 Constants.DEFAULT_FLUCTUATION_PERCENT
269 );
270 const phase2FluctuatedValue =
271 powerPerPhaseSampledValueTemplates?.L2?.value &&
272 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
273 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
274 powerPerPhaseSampledValueTemplates.L2.value,
275 connectorMaximumPowerPerPhase / unitDivider,
276 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 277 ),
78085c42
JB
278 powerPerPhaseSampledValueTemplates.L2.fluctuationPercent ??
279 Constants.DEFAULT_FLUCTUATION_PERCENT
280 );
281 const phase3FluctuatedValue =
282 powerPerPhaseSampledValueTemplates?.L3?.value &&
283 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
284 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
285 powerPerPhaseSampledValueTemplates.L3.value,
286 connectorMaximumPowerPerPhase / unitDivider,
287 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 288 ),
78085c42
JB
289 powerPerPhaseSampledValueTemplates.L3.fluctuationPercent ??
290 Constants.DEFAULT_FLUCTUATION_PERCENT
291 );
292 powerMeasurandValues.L1 =
293 phase1FluctuatedValue ??
294 defaultFluctuatedPowerPerPhase ??
ad8537a7 295 Utils.getRandomFloatRounded(connectorMaximumPowerPerPhase / unitDivider);
78085c42
JB
296 powerMeasurandValues.L2 =
297 phase2FluctuatedValue ??
298 defaultFluctuatedPowerPerPhase ??
ad8537a7 299 Utils.getRandomFloatRounded(connectorMaximumPowerPerPhase / unitDivider);
78085c42
JB
300 powerMeasurandValues.L3 =
301 phase3FluctuatedValue ??
302 defaultFluctuatedPowerPerPhase ??
ad8537a7 303 Utils.getRandomFloatRounded(connectorMaximumPowerPerPhase / unitDivider);
78085c42
JB
304 } else {
305 powerMeasurandValues.L1 = powerSampledValueTemplate.value
306 ? Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
307 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
308 powerSampledValueTemplate.value,
309 connectorMaximumPower / unitDivider,
310 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 311 ),
78085c42
JB
312 powerSampledValueTemplate.fluctuationPercent ??
313 Constants.DEFAULT_FLUCTUATION_PERCENT
314 )
ad8537a7 315 : Utils.getRandomFloatRounded(connectorMaximumPower / unitDivider);
78085c42
JB
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(
7bc31f9c
JB
327 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
328 powerSampledValueTemplate.value,
329 connectorMaximumPower / unitDivider,
330 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 331 ),
78085c42
JB
332 powerSampledValueTemplate.fluctuationPercent ??
333 Constants.DEFAULT_FLUCTUATION_PERCENT
334 )
ad8537a7 335 : Utils.getRandomFloatRounded(connectorMaximumPower / unitDivider);
78085c42
JB
336 break;
337 default:
fc040c43 338 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
78085c42
JB
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;
ad8537a7 348 const connectorMaximumPowerRounded = Utils.roundTo(connectorMaximumPower / unitDivider, 2);
78085c42 349 if (
71a77ac2 350 Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
ad8537a7 351 connectorMaximumPowerRounded ||
78085c42
JB
352 debug
353 ) {
354 logger.error(
355 `${chargingStation.logPrefix()} MeterValues measurand ${
356 meterValue.sampledValue[sampledValuesIndex].measurand ??
357 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
72092cfc 358 }: connectorId ${connectorId}, transaction ${connector?.transactionId}, value: ${
78085c42 359 meterValue.sampledValue[sampledValuesIndex].value
ad8537a7 360 }/${connectorMaximumPowerRounded}`
78085c42
JB
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,
72092cfc 374 undefined,
78085c42
JB
375 phaseValue as OCPP16MeterValuePhase
376 )
377 );
378 const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1;
ad8537a7
JB
379 const connectorMaximumPowerPerPhaseRounded = Utils.roundTo(
380 connectorMaximumPowerPerPhase / unitDivider,
381 2
382 );
78085c42
JB
383 if (
384 Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
ad8537a7 385 connectorMaximumPowerPerPhaseRounded ||
78085c42
JB
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
72092cfc 394 }, connectorId ${connectorId}, transaction ${connector?.transactionId}, value: ${
78085c42 395 meterValue.sampledValue[sampledValuesPerPhaseIndex].value
ad8537a7 396 }/${connectorMaximumPowerPerPhaseRounded}`
78085c42
JB
397 );
398 }
399 }
400 }
401 // Current.Import measurand
ed3d2808 402 const currentSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 403 chargingStation,
78085c42
JB
404 connectorId,
405 OCPP16MeterValueMeasurand.CURRENT_IMPORT
406 );
59b6ed8d
JB
407 let currentPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates =
408 Constants.EMPTY_OBJECT;
78085c42
JB
409 if (chargingStation.getNumberOfPhases() === 3) {
410 currentPerPhaseSampledValueTemplates = {
ed3d2808 411 L1: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 412 chargingStation,
78085c42
JB
413 connectorId,
414 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
415 OCPP16MeterValuePhase.L1
416 ),
ed3d2808 417 L2: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 418 chargingStation,
78085c42
JB
419 connectorId,
420 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
421 OCPP16MeterValuePhase.L2
422 ),
ed3d2808 423 L3: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 424 chargingStation,
78085c42
JB
425 connectorId,
426 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
427 OCPP16MeterValuePhase.L3
428 ),
429 };
430 }
431 if (currentSampledValueTemplate) {
432 OCPP16ServiceUtils.checkMeasurandPowerDivider(
433 chargingStation,
434 currentSampledValueTemplate.measurand
435 );
fc040c43 436 const errMsg = `MeterValues measurand ${
78085c42
JB
437 currentSampledValueTemplate.measurand ??
438 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
439 }: Unknown ${chargingStation.getCurrentOutType()} currentOutType in template file ${
2484ac1e 440 chargingStation.templateFile
78085c42
JB
441 }, cannot calculate ${
442 currentSampledValueTemplate.measurand ??
443 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
444 } measurand value`;
59b6ed8d 445 const currentMeasurandValues: MeasurandValues = Constants.EMPTY_OBJECT as MeasurandValues;
1b6498ba
JB
446 const connectorMaximumAvailablePower =
447 chargingStation.getConnectorMaximumAvailablePower(connectorId);
ad8537a7 448 let connectorMaximumAmperage: number;
78085c42
JB
449 switch (chargingStation.getCurrentOutType()) {
450 case CurrentType.AC:
ad8537a7 451 connectorMaximumAmperage = ACElectricUtils.amperagePerPhaseFromPower(
78085c42 452 chargingStation.getNumberOfPhases(),
1b6498ba 453 connectorMaximumAvailablePower,
78085c42
JB
454 chargingStation.getVoltageOut()
455 );
456 if (chargingStation.getNumberOfPhases() === 3) {
457 const defaultFluctuatedAmperagePerPhase =
458 currentSampledValueTemplate.value &&
459 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
460 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
461 currentSampledValueTemplate.value,
462 connectorMaximumAmperage,
463 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
464 ),
78085c42
JB
465 currentSampledValueTemplate.fluctuationPercent ??
466 Constants.DEFAULT_FLUCTUATION_PERCENT
467 );
468 const phase1FluctuatedValue =
469 currentPerPhaseSampledValueTemplates?.L1?.value &&
470 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
471 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
472 currentPerPhaseSampledValueTemplates.L1.value,
473 connectorMaximumAmperage,
474 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 475 ),
78085c42
JB
476 currentPerPhaseSampledValueTemplates.L1.fluctuationPercent ??
477 Constants.DEFAULT_FLUCTUATION_PERCENT
478 );
479 const phase2FluctuatedValue =
480 currentPerPhaseSampledValueTemplates?.L2?.value &&
481 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
482 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
483 currentPerPhaseSampledValueTemplates.L2.value,
484 connectorMaximumAmperage,
485 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 486 ),
78085c42
JB
487 currentPerPhaseSampledValueTemplates.L2.fluctuationPercent ??
488 Constants.DEFAULT_FLUCTUATION_PERCENT
489 );
490 const phase3FluctuatedValue =
491 currentPerPhaseSampledValueTemplates?.L3?.value &&
492 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
493 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
494 currentPerPhaseSampledValueTemplates.L3.value,
495 connectorMaximumAmperage,
496 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 497 ),
78085c42
JB
498 currentPerPhaseSampledValueTemplates.L3.fluctuationPercent ??
499 Constants.DEFAULT_FLUCTUATION_PERCENT
500 );
501 currentMeasurandValues.L1 =
502 phase1FluctuatedValue ??
503 defaultFluctuatedAmperagePerPhase ??
ad8537a7 504 Utils.getRandomFloatRounded(connectorMaximumAmperage);
78085c42
JB
505 currentMeasurandValues.L2 =
506 phase2FluctuatedValue ??
507 defaultFluctuatedAmperagePerPhase ??
ad8537a7 508 Utils.getRandomFloatRounded(connectorMaximumAmperage);
78085c42
JB
509 currentMeasurandValues.L3 =
510 phase3FluctuatedValue ??
511 defaultFluctuatedAmperagePerPhase ??
ad8537a7 512 Utils.getRandomFloatRounded(connectorMaximumAmperage);
78085c42
JB
513 } else {
514 currentMeasurandValues.L1 = currentSampledValueTemplate.value
515 ? Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
516 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
517 currentSampledValueTemplate.value,
518 connectorMaximumAmperage,
519 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
520 ),
78085c42
JB
521 currentSampledValueTemplate.fluctuationPercent ??
522 Constants.DEFAULT_FLUCTUATION_PERCENT
523 )
ad8537a7 524 : Utils.getRandomFloatRounded(connectorMaximumAmperage);
78085c42
JB
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:
ad8537a7 535 connectorMaximumAmperage = DCElectricUtils.amperage(
1b6498ba 536 connectorMaximumAvailablePower,
78085c42
JB
537 chargingStation.getVoltageOut()
538 );
539 currentMeasurandValues.allPhases = currentSampledValueTemplate.value
540 ? Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
541 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
542 currentSampledValueTemplate.value,
543 connectorMaximumAmperage,
544 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
545 ),
78085c42
JB
546 currentSampledValueTemplate.fluctuationPercent ??
547 Constants.DEFAULT_FLUCTUATION_PERCENT
548 )
ad8537a7 549 : Utils.getRandomFloatRounded(connectorMaximumAmperage);
78085c42
JB
550 break;
551 default:
fc040c43 552 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
78085c42
JB
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 (
ad8537a7
JB
563 Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
564 connectorMaximumAmperage ||
78085c42
JB
565 debug
566 ) {
567 logger.error(
568 `${chargingStation.logPrefix()} MeterValues measurand ${
569 meterValue.sampledValue[sampledValuesIndex].measurand ??
570 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
72092cfc 571 }: connectorId ${connectorId}, transaction ${connector?.transactionId}, value: ${
78085c42 572 meterValue.sampledValue[sampledValuesIndex].value
ad8537a7 573 }/${connectorMaximumAmperage}`
78085c42
JB
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,
72092cfc 587 undefined,
78085c42
JB
588 phaseValue as OCPP16MeterValuePhase
589 )
590 );
591 const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1;
592 if (
593 Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
ad8537a7 594 connectorMaximumAmperage ||
78085c42
JB
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
72092cfc 603 }, connectorId ${connectorId}, transaction ${connector?.transactionId}, value: ${
78085c42 604 meterValue.sampledValue[sampledValuesPerPhaseIndex].value
ad8537a7 605 }/${connectorMaximumAmperage}`
78085c42
JB
606 );
607 }
608 }
609 }
610 // Energy.Active.Import.Register measurand (default)
ed3d2808 611 const energySampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab
JB
612 chargingStation,
613 connectorId
614 );
78085c42
JB
615 if (energySampledValueTemplate) {
616 OCPP16ServiceUtils.checkMeasurandPowerDivider(
617 chargingStation,
618 energySampledValueTemplate.measurand
619 );
620 const unitDivider =
621 energySampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
1b6498ba
JB
622 const connectorMaximumAvailablePower =
623 chargingStation.getConnectorMaximumAvailablePower(connectorId);
ad8537a7 624 const connectorMaximumEnergyRounded = Utils.roundTo(
1b6498ba 625 (connectorMaximumAvailablePower * interval) / (3600 * 1000),
78085c42
JB
626 2
627 );
628 const energyValueRounded = energySampledValueTemplate.value
629 ? // Cumulate the fluctuated value around the static one
630 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
631 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
632 energySampledValueTemplate.value,
633 connectorMaximumEnergyRounded,
634 {
635 limitationEnabled: chargingStation.getCustomValueLimitationMeterValues(),
636 unitMultiplier: unitDivider,
637 }
34464008 638 ),
78085c42
JB
639 energySampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT
640 )
ad8537a7 641 : Utils.getRandomFloatRounded(connectorMaximumEnergyRounded);
78085c42
JB
642 // Persist previous value on connector
643 if (
644 connector &&
3a13fc92 645 Utils.isNullOrUndefined(connector.energyActiveImportRegisterValue) === false &&
78085c42 646 connector.energyActiveImportRegisterValue >= 0 &&
3a13fc92 647 Utils.isNullOrUndefined(connector.transactionEnergyActiveImportRegisterValue) === false &&
78085c42
JB
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;
ad8537a7 667 if (energyValueRounded > connectorMaximumEnergyRounded || debug) {
78085c42
JB
668 logger.error(
669 `${chargingStation.logPrefix()} MeterValues measurand ${
670 meterValue.sampledValue[sampledValuesIndex].measurand ??
671 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
672 }: connectorId ${connectorId}, transaction ${
72092cfc 673 connector?.transactionId
ad8537a7 674 }, value: ${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${Utils.roundTo(
78085c42
JB
675 interval / (3600 * 1000),
676 4
677 )}h`
678 );
679 }
680 }
681 return meterValue;
682 }
683
e7aeea18
JB
684 public static buildTransactionBeginMeterValue(
685 chargingStation: ChargingStation,
686 connectorId: number,
78085c42 687 meterStart: number
e7aeea18 688 ): OCPP16MeterValue {
fd0c36fa 689 const meterValue: OCPP16MeterValue = {
c38f0ced 690 timestamp: new Date(),
fd0c36fa
JB
691 sampledValue: [],
692 };
9ccca265 693 // Energy.Active.Import.Register measurand (default)
ed3d2808 694 const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab
JB
695 chargingStation,
696 connectorId
697 );
9ccca265 698 const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
e7aeea18
JB
699 meterValue.sampledValue.push(
700 OCPP16ServiceUtils.buildSampledValue(
701 sampledValueTemplate,
2040a613 702 Utils.roundTo((meterStart ?? 0) / unitDivider, 4),
e7aeea18
JB
703 MeterValueContext.TRANSACTION_BEGIN
704 )
705 );
fd0c36fa
JB
706 return meterValue;
707 }
708
e7aeea18
JB
709 public static buildTransactionEndMeterValue(
710 chargingStation: ChargingStation,
711 connectorId: number,
78085c42 712 meterStop: number
e7aeea18 713 ): OCPP16MeterValue {
fd0c36fa 714 const meterValue: OCPP16MeterValue = {
c38f0ced 715 timestamp: new Date(),
fd0c36fa
JB
716 sampledValue: [],
717 };
9ccca265 718 // Energy.Active.Import.Register measurand (default)
ed3d2808 719 const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab
JB
720 chargingStation,
721 connectorId
722 );
9ccca265 723 const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
e7aeea18
JB
724 meterValue.sampledValue.push(
725 OCPP16ServiceUtils.buildSampledValue(
726 sampledValueTemplate,
2040a613 727 Utils.roundTo((meterStop ?? 0) / unitDivider, 4),
e7aeea18
JB
728 MeterValueContext.TRANSACTION_END
729 )
730 );
fd0c36fa
JB
731 return meterValue;
732 }
733
e7aeea18
JB
734 public static buildTransactionDataMeterValues(
735 transactionBeginMeterValue: OCPP16MeterValue,
736 transactionEndMeterValue: OCPP16MeterValue
737 ): OCPP16MeterValue[] {
fd0c36fa
JB
738 const meterValues: OCPP16MeterValue[] = [];
739 meterValues.push(transactionBeginMeterValue);
740 meterValues.push(transactionEndMeterValue);
741 return meterValues;
742 }
7bc31f9c 743
ed3d2808
JB
744 public static setChargingProfile(
745 chargingStation: ChargingStation,
746 connectorId: number,
884a6fdf 747 cp: OCPP16ChargingProfile
ed3d2808 748 ): void {
72092cfc
JB
749 if (
750 Utils.isNullOrUndefined(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)
751 ) {
ed3d2808
JB
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 }
72092cfc
JB
757 if (
758 Array.isArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles) === false
759 ) {
ed3d2808
JB
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;
53ac516c 766 if (Utils.isNotEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
ed3d2808
JB
767 chargingStation
768 .getConnectorStatus(connectorId)
72092cfc 769 ?.chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
ed3d2808
JB
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 }
72092cfc 780 !cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp);
ed3d2808
JB
781 }
782
1b271a54
JB
783 public static parseJsonSchemaFile<T extends JsonType>(
784 relativePath: string,
785 moduleName?: string,
786 methodName?: string
787 ): JSONSchemaType<T> {
7164966d
JB
788 return super.parseJsonSchemaFile<T>(
789 path.resolve(path.dirname(fileURLToPath(import.meta.url)), relativePath),
1b271a54
JB
790 OCPPVersion.VERSION_16,
791 moduleName,
792 methodName
7164966d 793 );
130783a7
JB
794 }
795
7bc31f9c
JB
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 {
fa7bccf4 826 if (Utils.isUndefined(chargingStation.powerDivider)) {
fc040c43 827 const errMsg = `MeterValues measurand ${
7bc31f9c
JB
828 measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
829 }: powerDivider is undefined`;
fc040c43 830 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
7bc31f9c 831 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
fa7bccf4 832 } else if (chargingStation?.powerDivider <= 0) {
fc040c43 833 const errMsg = `MeterValues measurand ${
7bc31f9c 834 measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
fa7bccf4 835 }: powerDivider have zero or below value ${chargingStation.powerDivider}`;
fc040c43 836 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
7bc31f9c
JB
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 }
6ed92bc1 871}