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