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