fix: brown paper bag issue at referencing the same literal object instance
[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 );
abe9e9dd 199 let powerPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {};
78085c42
JB
200 if (chargingStation.getNumberOfPhases() === 3) {
201 powerPerPhaseSampledValueTemplates = {
ed3d2808 202 L1: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 203 chargingStation,
78085c42
JB
204 connectorId,
205 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
206 OCPP16MeterValuePhase.L1_N
207 ),
ed3d2808 208 L2: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 209 chargingStation,
78085c42
JB
210 connectorId,
211 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
212 OCPP16MeterValuePhase.L2_N
213 ),
ed3d2808 214 L3: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 215 chargingStation,
78085c42
JB
216 connectorId,
217 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
218 OCPP16MeterValuePhase.L3_N
219 ),
220 };
221 }
222 if (powerSampledValueTemplate) {
223 OCPP16ServiceUtils.checkMeasurandPowerDivider(
224 chargingStation,
225 powerSampledValueTemplate.measurand
226 );
fc040c43 227 const errMsg = `MeterValues measurand ${
78085c42
JB
228 powerSampledValueTemplate.measurand ??
229 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
230 }: Unknown ${chargingStation.getCurrentOutType()} currentOutType in template file ${
2484ac1e 231 chargingStation.templateFile
78085c42
JB
232 }, cannot calculate ${
233 powerSampledValueTemplate.measurand ??
234 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
235 } measurand value`;
abe9e9dd 236 const powerMeasurandValues = {} as MeasurandValues;
78085c42 237 const unitDivider = powerSampledValueTemplate?.unit === MeterValueUnit.KILO_WATT ? 1000 : 1;
1b6498ba
JB
238 const connectorMaximumAvailablePower =
239 chargingStation.getConnectorMaximumAvailablePower(connectorId);
240 const connectorMaximumPower = Math.round(connectorMaximumAvailablePower);
ad8537a7 241 const connectorMaximumPowerPerPhase = Math.round(
1b6498ba 242 connectorMaximumAvailablePower / chargingStation.getNumberOfPhases()
78085c42
JB
243 );
244 switch (chargingStation.getCurrentOutType()) {
245 case CurrentType.AC:
246 if (chargingStation.getNumberOfPhases() === 3) {
247 const defaultFluctuatedPowerPerPhase =
248 powerSampledValueTemplate.value &&
249 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
250 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
251 powerSampledValueTemplate.value,
252 connectorMaximumPower / unitDivider,
253 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 254 ) / chargingStation.getNumberOfPhases(),
78085c42
JB
255 powerSampledValueTemplate.fluctuationPercent ??
256 Constants.DEFAULT_FLUCTUATION_PERCENT
257 );
258 const phase1FluctuatedValue =
259 powerPerPhaseSampledValueTemplates?.L1?.value &&
260 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
261 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
262 powerPerPhaseSampledValueTemplates.L1.value,
263 connectorMaximumPowerPerPhase / unitDivider,
264 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 265 ),
78085c42
JB
266 powerPerPhaseSampledValueTemplates.L1.fluctuationPercent ??
267 Constants.DEFAULT_FLUCTUATION_PERCENT
268 );
269 const phase2FluctuatedValue =
270 powerPerPhaseSampledValueTemplates?.L2?.value &&
271 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
272 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
273 powerPerPhaseSampledValueTemplates.L2.value,
274 connectorMaximumPowerPerPhase / unitDivider,
275 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 276 ),
78085c42
JB
277 powerPerPhaseSampledValueTemplates.L2.fluctuationPercent ??
278 Constants.DEFAULT_FLUCTUATION_PERCENT
279 );
280 const phase3FluctuatedValue =
281 powerPerPhaseSampledValueTemplates?.L3?.value &&
282 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
283 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
284 powerPerPhaseSampledValueTemplates.L3.value,
285 connectorMaximumPowerPerPhase / unitDivider,
286 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 287 ),
78085c42
JB
288 powerPerPhaseSampledValueTemplates.L3.fluctuationPercent ??
289 Constants.DEFAULT_FLUCTUATION_PERCENT
290 );
291 powerMeasurandValues.L1 =
292 phase1FluctuatedValue ??
293 defaultFluctuatedPowerPerPhase ??
ad8537a7 294 Utils.getRandomFloatRounded(connectorMaximumPowerPerPhase / unitDivider);
78085c42
JB
295 powerMeasurandValues.L2 =
296 phase2FluctuatedValue ??
297 defaultFluctuatedPowerPerPhase ??
ad8537a7 298 Utils.getRandomFloatRounded(connectorMaximumPowerPerPhase / unitDivider);
78085c42
JB
299 powerMeasurandValues.L3 =
300 phase3FluctuatedValue ??
301 defaultFluctuatedPowerPerPhase ??
ad8537a7 302 Utils.getRandomFloatRounded(connectorMaximumPowerPerPhase / unitDivider);
78085c42
JB
303 } else {
304 powerMeasurandValues.L1 = powerSampledValueTemplate.value
305 ? Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
306 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
307 powerSampledValueTemplate.value,
308 connectorMaximumPower / unitDivider,
309 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 310 ),
78085c42
JB
311 powerSampledValueTemplate.fluctuationPercent ??
312 Constants.DEFAULT_FLUCTUATION_PERCENT
313 )
ad8537a7 314 : Utils.getRandomFloatRounded(connectorMaximumPower / unitDivider);
78085c42
JB
315 powerMeasurandValues.L2 = 0;
316 powerMeasurandValues.L3 = 0;
317 }
318 powerMeasurandValues.allPhases = Utils.roundTo(
319 powerMeasurandValues.L1 + powerMeasurandValues.L2 + powerMeasurandValues.L3,
320 2
321 );
322 break;
323 case CurrentType.DC:
324 powerMeasurandValues.allPhases = powerSampledValueTemplate.value
325 ? Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
326 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
327 powerSampledValueTemplate.value,
328 connectorMaximumPower / unitDivider,
329 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 330 ),
78085c42
JB
331 powerSampledValueTemplate.fluctuationPercent ??
332 Constants.DEFAULT_FLUCTUATION_PERCENT
333 )
ad8537a7 334 : Utils.getRandomFloatRounded(connectorMaximumPower / unitDivider);
78085c42
JB
335 break;
336 default:
fc040c43 337 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
78085c42
JB
338 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
339 }
340 meterValue.sampledValue.push(
341 OCPP16ServiceUtils.buildSampledValue(
342 powerSampledValueTemplate,
343 powerMeasurandValues.allPhases
344 )
345 );
346 const sampledValuesIndex = meterValue.sampledValue.length - 1;
ad8537a7 347 const connectorMaximumPowerRounded = Utils.roundTo(connectorMaximumPower / unitDivider, 2);
78085c42 348 if (
71a77ac2 349 Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
ad8537a7 350 connectorMaximumPowerRounded ||
78085c42
JB
351 debug
352 ) {
353 logger.error(
354 `${chargingStation.logPrefix()} MeterValues measurand ${
355 meterValue.sampledValue[sampledValuesIndex].measurand ??
356 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
72092cfc 357 }: connectorId ${connectorId}, transaction ${connector?.transactionId}, value: ${
78085c42 358 meterValue.sampledValue[sampledValuesIndex].value
ad8537a7 359 }/${connectorMaximumPowerRounded}`
78085c42
JB
360 );
361 }
362 for (
363 let phase = 1;
364 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
365 phase++
366 ) {
367 const phaseValue = `L${phase}-N`;
368 meterValue.sampledValue.push(
369 OCPP16ServiceUtils.buildSampledValue(
370 (powerPerPhaseSampledValueTemplates[`L${phase}`] as SampledValueTemplate) ??
371 powerSampledValueTemplate,
372 powerMeasurandValues[`L${phase}`] as number,
72092cfc 373 undefined,
78085c42
JB
374 phaseValue as OCPP16MeterValuePhase
375 )
376 );
377 const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1;
ad8537a7
JB
378 const connectorMaximumPowerPerPhaseRounded = Utils.roundTo(
379 connectorMaximumPowerPerPhase / unitDivider,
380 2
381 );
78085c42
JB
382 if (
383 Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
ad8537a7 384 connectorMaximumPowerPerPhaseRounded ||
78085c42
JB
385 debug
386 ) {
387 logger.error(
388 `${chargingStation.logPrefix()} MeterValues measurand ${
389 meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ??
390 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
391 }: phase ${
392 meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
72092cfc 393 }, connectorId ${connectorId}, transaction ${connector?.transactionId}, value: ${
78085c42 394 meterValue.sampledValue[sampledValuesPerPhaseIndex].value
ad8537a7 395 }/${connectorMaximumPowerPerPhaseRounded}`
78085c42
JB
396 );
397 }
398 }
399 }
400 // Current.Import measurand
ed3d2808 401 const currentSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 402 chargingStation,
78085c42
JB
403 connectorId,
404 OCPP16MeterValueMeasurand.CURRENT_IMPORT
405 );
abe9e9dd 406 let currentPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {};
78085c42
JB
407 if (chargingStation.getNumberOfPhases() === 3) {
408 currentPerPhaseSampledValueTemplates = {
ed3d2808 409 L1: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 410 chargingStation,
78085c42
JB
411 connectorId,
412 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
413 OCPP16MeterValuePhase.L1
414 ),
ed3d2808 415 L2: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 416 chargingStation,
78085c42
JB
417 connectorId,
418 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
419 OCPP16MeterValuePhase.L2
420 ),
ed3d2808 421 L3: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 422 chargingStation,
78085c42
JB
423 connectorId,
424 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
425 OCPP16MeterValuePhase.L3
426 ),
427 };
428 }
429 if (currentSampledValueTemplate) {
430 OCPP16ServiceUtils.checkMeasurandPowerDivider(
431 chargingStation,
432 currentSampledValueTemplate.measurand
433 );
fc040c43 434 const errMsg = `MeterValues measurand ${
78085c42
JB
435 currentSampledValueTemplate.measurand ??
436 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
437 }: Unknown ${chargingStation.getCurrentOutType()} currentOutType in template file ${
2484ac1e 438 chargingStation.templateFile
78085c42
JB
439 }, cannot calculate ${
440 currentSampledValueTemplate.measurand ??
441 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
442 } measurand value`;
abe9e9dd 443 const currentMeasurandValues: MeasurandValues = {} as MeasurandValues;
1b6498ba
JB
444 const connectorMaximumAvailablePower =
445 chargingStation.getConnectorMaximumAvailablePower(connectorId);
ad8537a7 446 let connectorMaximumAmperage: number;
78085c42
JB
447 switch (chargingStation.getCurrentOutType()) {
448 case CurrentType.AC:
ad8537a7 449 connectorMaximumAmperage = ACElectricUtils.amperagePerPhaseFromPower(
78085c42 450 chargingStation.getNumberOfPhases(),
1b6498ba 451 connectorMaximumAvailablePower,
78085c42
JB
452 chargingStation.getVoltageOut()
453 );
454 if (chargingStation.getNumberOfPhases() === 3) {
455 const defaultFluctuatedAmperagePerPhase =
456 currentSampledValueTemplate.value &&
457 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
458 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
459 currentSampledValueTemplate.value,
460 connectorMaximumAmperage,
461 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
462 ),
78085c42
JB
463 currentSampledValueTemplate.fluctuationPercent ??
464 Constants.DEFAULT_FLUCTUATION_PERCENT
465 );
466 const phase1FluctuatedValue =
467 currentPerPhaseSampledValueTemplates?.L1?.value &&
468 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
469 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
470 currentPerPhaseSampledValueTemplates.L1.value,
471 connectorMaximumAmperage,
472 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 473 ),
78085c42
JB
474 currentPerPhaseSampledValueTemplates.L1.fluctuationPercent ??
475 Constants.DEFAULT_FLUCTUATION_PERCENT
476 );
477 const phase2FluctuatedValue =
478 currentPerPhaseSampledValueTemplates?.L2?.value &&
479 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
480 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
481 currentPerPhaseSampledValueTemplates.L2.value,
482 connectorMaximumAmperage,
483 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 484 ),
78085c42
JB
485 currentPerPhaseSampledValueTemplates.L2.fluctuationPercent ??
486 Constants.DEFAULT_FLUCTUATION_PERCENT
487 );
488 const phase3FluctuatedValue =
489 currentPerPhaseSampledValueTemplates?.L3?.value &&
490 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
491 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
492 currentPerPhaseSampledValueTemplates.L3.value,
493 connectorMaximumAmperage,
494 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
34464008 495 ),
78085c42
JB
496 currentPerPhaseSampledValueTemplates.L3.fluctuationPercent ??
497 Constants.DEFAULT_FLUCTUATION_PERCENT
498 );
499 currentMeasurandValues.L1 =
500 phase1FluctuatedValue ??
501 defaultFluctuatedAmperagePerPhase ??
ad8537a7 502 Utils.getRandomFloatRounded(connectorMaximumAmperage);
78085c42
JB
503 currentMeasurandValues.L2 =
504 phase2FluctuatedValue ??
505 defaultFluctuatedAmperagePerPhase ??
ad8537a7 506 Utils.getRandomFloatRounded(connectorMaximumAmperage);
78085c42
JB
507 currentMeasurandValues.L3 =
508 phase3FluctuatedValue ??
509 defaultFluctuatedAmperagePerPhase ??
ad8537a7 510 Utils.getRandomFloatRounded(connectorMaximumAmperage);
78085c42
JB
511 } else {
512 currentMeasurandValues.L1 = currentSampledValueTemplate.value
513 ? Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
514 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
515 currentSampledValueTemplate.value,
516 connectorMaximumAmperage,
517 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
518 ),
78085c42
JB
519 currentSampledValueTemplate.fluctuationPercent ??
520 Constants.DEFAULT_FLUCTUATION_PERCENT
521 )
ad8537a7 522 : Utils.getRandomFloatRounded(connectorMaximumAmperage);
78085c42
JB
523 currentMeasurandValues.L2 = 0;
524 currentMeasurandValues.L3 = 0;
525 }
526 currentMeasurandValues.allPhases = Utils.roundTo(
527 (currentMeasurandValues.L1 + currentMeasurandValues.L2 + currentMeasurandValues.L3) /
528 chargingStation.getNumberOfPhases(),
529 2
530 );
531 break;
532 case CurrentType.DC:
ad8537a7 533 connectorMaximumAmperage = DCElectricUtils.amperage(
1b6498ba 534 connectorMaximumAvailablePower,
78085c42
JB
535 chargingStation.getVoltageOut()
536 );
537 currentMeasurandValues.allPhases = currentSampledValueTemplate.value
538 ? Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
539 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
540 currentSampledValueTemplate.value,
541 connectorMaximumAmperage,
542 { limitationEnabled: chargingStation.getCustomValueLimitationMeterValues() }
543 ),
78085c42
JB
544 currentSampledValueTemplate.fluctuationPercent ??
545 Constants.DEFAULT_FLUCTUATION_PERCENT
546 )
ad8537a7 547 : Utils.getRandomFloatRounded(connectorMaximumAmperage);
78085c42
JB
548 break;
549 default:
fc040c43 550 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
78085c42
JB
551 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
552 }
553 meterValue.sampledValue.push(
554 OCPP16ServiceUtils.buildSampledValue(
555 currentSampledValueTemplate,
556 currentMeasurandValues.allPhases
557 )
558 );
559 const sampledValuesIndex = meterValue.sampledValue.length - 1;
560 if (
ad8537a7
JB
561 Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
562 connectorMaximumAmperage ||
78085c42
JB
563 debug
564 ) {
565 logger.error(
566 `${chargingStation.logPrefix()} MeterValues measurand ${
567 meterValue.sampledValue[sampledValuesIndex].measurand ??
568 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
72092cfc 569 }: connectorId ${connectorId}, transaction ${connector?.transactionId}, value: ${
78085c42 570 meterValue.sampledValue[sampledValuesIndex].value
ad8537a7 571 }/${connectorMaximumAmperage}`
78085c42
JB
572 );
573 }
574 for (
575 let phase = 1;
576 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
577 phase++
578 ) {
579 const phaseValue = `L${phase}`;
580 meterValue.sampledValue.push(
581 OCPP16ServiceUtils.buildSampledValue(
582 (currentPerPhaseSampledValueTemplates[phaseValue] as SampledValueTemplate) ??
583 currentSampledValueTemplate,
584 currentMeasurandValues[phaseValue] as number,
72092cfc 585 undefined,
78085c42
JB
586 phaseValue as OCPP16MeterValuePhase
587 )
588 );
589 const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1;
590 if (
591 Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
ad8537a7 592 connectorMaximumAmperage ||
78085c42
JB
593 debug
594 ) {
595 logger.error(
596 `${chargingStation.logPrefix()} MeterValues measurand ${
597 meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ??
598 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
599 }: phase ${
600 meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
72092cfc 601 }, connectorId ${connectorId}, transaction ${connector?.transactionId}, value: ${
78085c42 602 meterValue.sampledValue[sampledValuesPerPhaseIndex].value
ad8537a7 603 }/${connectorMaximumAmperage}`
78085c42
JB
604 );
605 }
606 }
607 }
608 // Energy.Active.Import.Register measurand (default)
ed3d2808 609 const energySampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab
JB
610 chargingStation,
611 connectorId
612 );
78085c42
JB
613 if (energySampledValueTemplate) {
614 OCPP16ServiceUtils.checkMeasurandPowerDivider(
615 chargingStation,
616 energySampledValueTemplate.measurand
617 );
618 const unitDivider =
619 energySampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
1b6498ba
JB
620 const connectorMaximumAvailablePower =
621 chargingStation.getConnectorMaximumAvailablePower(connectorId);
ad8537a7 622 const connectorMaximumEnergyRounded = Utils.roundTo(
1b6498ba 623 (connectorMaximumAvailablePower * interval) / (3600 * 1000),
78085c42
JB
624 2
625 );
626 const energyValueRounded = energySampledValueTemplate.value
627 ? // Cumulate the fluctuated value around the static one
628 Utils.getRandomFloatFluctuatedRounded(
7bc31f9c
JB
629 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
630 energySampledValueTemplate.value,
631 connectorMaximumEnergyRounded,
632 {
633 limitationEnabled: chargingStation.getCustomValueLimitationMeterValues(),
634 unitMultiplier: unitDivider,
635 }
34464008 636 ),
78085c42
JB
637 energySampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT
638 )
ad8537a7 639 : Utils.getRandomFloatRounded(connectorMaximumEnergyRounded);
78085c42
JB
640 // Persist previous value on connector
641 if (
642 connector &&
3a13fc92 643 Utils.isNullOrUndefined(connector.energyActiveImportRegisterValue) === false &&
78085c42 644 connector.energyActiveImportRegisterValue >= 0 &&
3a13fc92 645 Utils.isNullOrUndefined(connector.transactionEnergyActiveImportRegisterValue) === false &&
78085c42
JB
646 connector.transactionEnergyActiveImportRegisterValue >= 0
647 ) {
648 connector.energyActiveImportRegisterValue += energyValueRounded;
649 connector.transactionEnergyActiveImportRegisterValue += energyValueRounded;
650 } else {
651 connector.energyActiveImportRegisterValue = 0;
652 connector.transactionEnergyActiveImportRegisterValue = 0;
653 }
654 meterValue.sampledValue.push(
655 OCPP16ServiceUtils.buildSampledValue(
656 energySampledValueTemplate,
657 Utils.roundTo(
658 chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) /
659 unitDivider,
660 2
661 )
662 )
663 );
664 const sampledValuesIndex = meterValue.sampledValue.length - 1;
ad8537a7 665 if (energyValueRounded > connectorMaximumEnergyRounded || debug) {
78085c42
JB
666 logger.error(
667 `${chargingStation.logPrefix()} MeterValues measurand ${
668 meterValue.sampledValue[sampledValuesIndex].measurand ??
669 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
670 }: connectorId ${connectorId}, transaction ${
72092cfc 671 connector?.transactionId
ad8537a7 672 }, value: ${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${Utils.roundTo(
78085c42
JB
673 interval / (3600 * 1000),
674 4
675 )}h`
676 );
677 }
678 }
679 return meterValue;
680 }
681
e7aeea18
JB
682 public static buildTransactionBeginMeterValue(
683 chargingStation: ChargingStation,
684 connectorId: number,
78085c42 685 meterStart: number
e7aeea18 686 ): OCPP16MeterValue {
fd0c36fa 687 const meterValue: OCPP16MeterValue = {
c38f0ced 688 timestamp: new Date(),
fd0c36fa
JB
689 sampledValue: [],
690 };
9ccca265 691 // Energy.Active.Import.Register measurand (default)
ed3d2808 692 const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab
JB
693 chargingStation,
694 connectorId
695 );
9ccca265 696 const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
e7aeea18
JB
697 meterValue.sampledValue.push(
698 OCPP16ServiceUtils.buildSampledValue(
699 sampledValueTemplate,
2040a613 700 Utils.roundTo((meterStart ?? 0) / unitDivider, 4),
e7aeea18
JB
701 MeterValueContext.TRANSACTION_BEGIN
702 )
703 );
fd0c36fa
JB
704 return meterValue;
705 }
706
e7aeea18
JB
707 public static buildTransactionEndMeterValue(
708 chargingStation: ChargingStation,
709 connectorId: number,
78085c42 710 meterStop: number
e7aeea18 711 ): OCPP16MeterValue {
fd0c36fa 712 const meterValue: OCPP16MeterValue = {
c38f0ced 713 timestamp: new Date(),
fd0c36fa
JB
714 sampledValue: [],
715 };
9ccca265 716 // Energy.Active.Import.Register measurand (default)
ed3d2808 717 const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab
JB
718 chargingStation,
719 connectorId
720 );
9ccca265 721 const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
e7aeea18
JB
722 meterValue.sampledValue.push(
723 OCPP16ServiceUtils.buildSampledValue(
724 sampledValueTemplate,
2040a613 725 Utils.roundTo((meterStop ?? 0) / unitDivider, 4),
e7aeea18
JB
726 MeterValueContext.TRANSACTION_END
727 )
728 );
fd0c36fa
JB
729 return meterValue;
730 }
731
e7aeea18
JB
732 public static buildTransactionDataMeterValues(
733 transactionBeginMeterValue: OCPP16MeterValue,
734 transactionEndMeterValue: OCPP16MeterValue
735 ): OCPP16MeterValue[] {
fd0c36fa
JB
736 const meterValues: OCPP16MeterValue[] = [];
737 meterValues.push(transactionBeginMeterValue);
738 meterValues.push(transactionEndMeterValue);
739 return meterValues;
740 }
7bc31f9c 741
ed3d2808
JB
742 public static setChargingProfile(
743 chargingStation: ChargingStation,
744 connectorId: number,
884a6fdf 745 cp: OCPP16ChargingProfile
ed3d2808 746 ): void {
72092cfc
JB
747 if (
748 Utils.isNullOrUndefined(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)
749 ) {
ed3d2808
JB
750 logger.error(
751 `${chargingStation.logPrefix()} Trying to set a charging profile on connectorId ${connectorId} with an uninitialized charging profiles array attribute, applying deferred initialization`
752 );
753 chargingStation.getConnectorStatus(connectorId).chargingProfiles = [];
754 }
72092cfc
JB
755 if (
756 Array.isArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles) === false
757 ) {
ed3d2808
JB
758 logger.error(
759 `${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`
760 );
761 chargingStation.getConnectorStatus(connectorId).chargingProfiles = [];
762 }
763 let cpReplaced = false;
53ac516c 764 if (Utils.isNotEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
ed3d2808
JB
765 chargingStation
766 .getConnectorStatus(connectorId)
72092cfc 767 ?.chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
ed3d2808
JB
768 if (
769 chargingProfile.chargingProfileId === cp.chargingProfileId ||
770 (chargingProfile.stackLevel === cp.stackLevel &&
771 chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)
772 ) {
773 chargingStation.getConnectorStatus(connectorId).chargingProfiles[index] = cp;
774 cpReplaced = true;
775 }
776 });
777 }
72092cfc 778 !cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp);
ed3d2808
JB
779 }
780
1b271a54
JB
781 public static parseJsonSchemaFile<T extends JsonType>(
782 relativePath: string,
783 moduleName?: string,
784 methodName?: string
785 ): JSONSchemaType<T> {
7164966d
JB
786 return super.parseJsonSchemaFile<T>(
787 path.resolve(path.dirname(fileURLToPath(import.meta.url)), relativePath),
1b271a54
JB
788 OCPPVersion.VERSION_16,
789 moduleName,
790 methodName
7164966d 791 );
130783a7
JB
792 }
793
7bc31f9c
JB
794 private static buildSampledValue(
795 sampledValueTemplate: SampledValueTemplate,
796 value: number,
797 context?: MeterValueContext,
798 phase?: OCPP16MeterValuePhase
799 ): OCPP16SampledValue {
800 const sampledValueValue = value ?? sampledValueTemplate?.value ?? null;
801 const sampledValueContext = context ?? sampledValueTemplate?.context ?? null;
802 const sampledValueLocation =
803 sampledValueTemplate?.location ??
804 OCPP16ServiceUtils.getMeasurandDefaultLocation(sampledValueTemplate?.measurand ?? null);
805 const sampledValuePhase = phase ?? sampledValueTemplate?.phase ?? null;
806 return {
807 ...(!Utils.isNullOrUndefined(sampledValueTemplate.unit) && {
808 unit: sampledValueTemplate.unit,
809 }),
810 ...(!Utils.isNullOrUndefined(sampledValueContext) && { context: sampledValueContext }),
811 ...(!Utils.isNullOrUndefined(sampledValueTemplate.measurand) && {
812 measurand: sampledValueTemplate.measurand,
813 }),
814 ...(!Utils.isNullOrUndefined(sampledValueLocation) && { location: sampledValueLocation }),
815 ...(!Utils.isNullOrUndefined(sampledValueValue) && { value: sampledValueValue.toString() }),
816 ...(!Utils.isNullOrUndefined(sampledValuePhase) && { phase: sampledValuePhase }),
817 };
818 }
819
820 private static checkMeasurandPowerDivider(
821 chargingStation: ChargingStation,
822 measurandType: OCPP16MeterValueMeasurand
823 ): void {
fa7bccf4 824 if (Utils.isUndefined(chargingStation.powerDivider)) {
fc040c43 825 const errMsg = `MeterValues measurand ${
7bc31f9c
JB
826 measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
827 }: powerDivider is undefined`;
fc040c43 828 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
7bc31f9c 829 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
fa7bccf4 830 } else if (chargingStation?.powerDivider <= 0) {
fc040c43 831 const errMsg = `MeterValues measurand ${
7bc31f9c 832 measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
fa7bccf4 833 }: powerDivider have zero or below value ${chargingStation.powerDivider}`;
fc040c43 834 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
7bc31f9c
JB
835 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
836 }
837 }
838
839 private static getMeasurandDefaultLocation(
840 measurandType: OCPP16MeterValueMeasurand
841 ): MeterValueLocation | undefined {
842 switch (measurandType) {
843 case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
844 return MeterValueLocation.EV;
845 }
846 }
847
848 private static getMeasurandDefaultUnit(
849 measurandType: OCPP16MeterValueMeasurand
850 ): MeterValueUnit | undefined {
851 switch (measurandType) {
852 case OCPP16MeterValueMeasurand.CURRENT_EXPORT:
853 case OCPP16MeterValueMeasurand.CURRENT_IMPORT:
854 case OCPP16MeterValueMeasurand.CURRENT_OFFERED:
855 return MeterValueUnit.AMP;
856 case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER:
857 case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER:
858 return MeterValueUnit.WATT_HOUR;
859 case OCPP16MeterValueMeasurand.POWER_ACTIVE_EXPORT:
860 case OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT:
861 case OCPP16MeterValueMeasurand.POWER_OFFERED:
862 return MeterValueUnit.WATT;
863 case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
864 return MeterValueUnit.PERCENT;
865 case OCPP16MeterValueMeasurand.VOLTAGE:
866 return MeterValueUnit.VOLT;
867 }
868 }
6ed92bc1 869}