fix: ensure built sample meterValues value can't be overriden
[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 3import type { JSONSchemaType } from 'ajv';
ef9e3b33
JB
4import {
5 addSeconds,
6 areIntervalsOverlapping,
7 differenceInSeconds,
8 isAfter,
9 isBefore,
10 isWithinInterval,
11} from 'date-fns';
130783a7 12
366f75f6 13import { OCPP16Constants } from './OCPP16Constants';
90aceaf6
JB
14import {
15 type ChargingStation,
16 hasFeatureProfile,
17 hasReservationExpired,
18} from '../../../charging-station';
268a74bb 19import { OCPPError } from '../../../exception';
e7aeea18 20import {
73d87be1 21 type ClearChargingProfileRequest,
268a74bb
JB
22 CurrentType,
23 ErrorType,
d19b10a8 24 type GenericResponse,
268a74bb
JB
25 type JsonType,
26 type MeasurandPerPhaseSampledValueTemplates,
27 type MeasurandValues,
e7aeea18
JB
28 MeterValueContext,
29 MeterValueLocation,
30 MeterValueUnit,
d19b10a8 31 OCPP16AuthorizationStatus,
366f75f6
JB
32 OCPP16AvailabilityType,
33 type OCPP16ChangeAvailabilityResponse,
34 OCPP16ChargePointStatus,
268a74bb 35 type OCPP16ChargingProfile,
ef9e3b33 36 type OCPP16ChargingSchedule,
268a74bb 37 type OCPP16IncomingRequestCommand,
27782dbc 38 type OCPP16MeterValue,
e7aeea18
JB
39 OCPP16MeterValueMeasurand,
40 OCPP16MeterValuePhase,
370ae4ee 41 OCPP16RequestCommand,
268a74bb
JB
42 type OCPP16SampledValue,
43 OCPP16StandardParametersKey,
d19b10a8 44 OCPP16StopTransactionReason,
268a74bb
JB
45 type OCPP16SupportedFeatureProfiles,
46 OCPPVersion,
47 type SampledValueTemplate,
268a74bb 48} from '../../../types';
9bf0ef23
JB
49import {
50 ACElectricUtils,
51 Constants,
52 DCElectricUtils,
53 convertToFloat,
54 convertToInt,
55 getRandomFloatFluctuatedRounded,
56 getRandomFloatRounded,
57 getRandomInteger,
58 isNotEmptyArray,
5a47f72c 59 isNotEmptyString,
9bf0ef23
JB
60 isNullOrUndefined,
61 isUndefined,
62 logger,
63 roundTo,
64} from '../../../utils';
4c3c0d59 65import { OCPPServiceUtils } from '../OCPPServiceUtils';
6ed92bc1 66
7bc31f9c 67export class OCPP16ServiceUtils extends OCPPServiceUtils {
370ae4ee
JB
68 public static checkFeatureProfile(
69 chargingStation: ChargingStation,
70 featureProfile: OCPP16SupportedFeatureProfiles,
5edd8ba0 71 command: OCPP16RequestCommand | OCPP16IncomingRequestCommand,
370ae4ee 72 ): boolean {
d8093be1 73 if (!hasFeatureProfile(chargingStation, featureProfile)) {
370ae4ee
JB
74 logger.warn(
75 `${chargingStation.logPrefix()} Trying to '${command}' without '${featureProfile}' feature enabled in ${
76 OCPP16StandardParametersKey.SupportedFeatureProfiles
5edd8ba0 77 } in configuration`,
370ae4ee
JB
78 );
79 return false;
80 }
81 return true;
82 }
83
78085c42
JB
84 public static buildMeterValue(
85 chargingStation: ChargingStation,
86 connectorId: number,
87 transactionId: number,
88 interval: number,
5edd8ba0 89 debug = false,
78085c42
JB
90 ): OCPP16MeterValue {
91 const meterValue: OCPP16MeterValue = {
c38f0ced 92 timestamp: new Date(),
78085c42
JB
93 sampledValue: [],
94 };
95 const connector = chargingStation.getConnectorStatus(connectorId);
96 // SoC measurand
ed3d2808 97 const socSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 98 chargingStation,
78085c42 99 connectorId,
5edd8ba0 100 OCPP16MeterValueMeasurand.STATE_OF_CHARGE,
78085c42
JB
101 );
102 if (socSampledValueTemplate) {
860ef183
JB
103 const socMaximumValue = 100;
104 const socMinimumValue = socSampledValueTemplate.minimumValue ?? 0;
5a47f72c 105 const socSampledValueTemplateValue = isNotEmptyString(socSampledValueTemplate.value)
9bf0ef23 106 ? getRandomFloatFluctuatedRounded(
78085c42 107 parseInt(socSampledValueTemplate.value),
5edd8ba0 108 socSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT,
78085c42 109 )
9bf0ef23 110 : getRandomInteger(socMaximumValue, socMinimumValue);
78085c42 111 meterValue.sampledValue.push(
5edd8ba0 112 OCPP16ServiceUtils.buildSampledValue(socSampledValueTemplate, socSampledValueTemplateValue),
78085c42
JB
113 );
114 const sampledValuesIndex = meterValue.sampledValue.length - 1;
860ef183 115 if (
9bf0ef23
JB
116 convertToInt(meterValue.sampledValue[sampledValuesIndex].value) > socMaximumValue ||
117 convertToInt(meterValue.sampledValue[sampledValuesIndex].value) < socMinimumValue ||
860ef183
JB
118 debug
119 ) {
78085c42
JB
120 logger.error(
121 `${chargingStation.logPrefix()} MeterValues measurand ${
122 meterValue.sampledValue[sampledValuesIndex].measurand ??
123 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
5edd8ba0 124 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${socMinimumValue}/${
78085c42 125 meterValue.sampledValue[sampledValuesIndex].value
9ff486f4 126 }/${socMaximumValue}`,
78085c42
JB
127 );
128 }
129 }
130 // Voltage measurand
ed3d2808 131 const voltageSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 132 chargingStation,
78085c42 133 connectorId,
5edd8ba0 134 OCPP16MeterValueMeasurand.VOLTAGE,
78085c42
JB
135 );
136 if (voltageSampledValueTemplate) {
5a47f72c 137 const voltageSampledValueTemplateValue = isNotEmptyString(voltageSampledValueTemplate.value)
78085c42 138 ? parseInt(voltageSampledValueTemplate.value)
5398cecf 139 : chargingStation.stationInfo.voltageOut!;
78085c42
JB
140 const fluctuationPercent =
141 voltageSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT;
9bf0ef23 142 const voltageMeasurandValue = getRandomFloatFluctuatedRounded(
78085c42 143 voltageSampledValueTemplateValue,
5edd8ba0 144 fluctuationPercent,
78085c42
JB
145 );
146 if (
147 chargingStation.getNumberOfPhases() !== 3 ||
5398cecf
JB
148 (chargingStation.getNumberOfPhases() === 3 &&
149 chargingStation.stationInfo?.mainVoltageMeterValues)
78085c42
JB
150 ) {
151 meterValue.sampledValue.push(
5edd8ba0 152 OCPP16ServiceUtils.buildSampledValue(voltageSampledValueTemplate, voltageMeasurandValue),
78085c42
JB
153 );
154 }
155 for (
156 let phase = 1;
157 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
158 phase++
159 ) {
160 const phaseLineToNeutralValue = `L${phase}-N`;
161 const voltagePhaseLineToNeutralSampledValueTemplate =
ed3d2808 162 OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 163 chargingStation,
78085c42
JB
164 connectorId,
165 OCPP16MeterValueMeasurand.VOLTAGE,
5edd8ba0 166 phaseLineToNeutralValue as OCPP16MeterValuePhase,
78085c42 167 );
e1d9a0f4 168 let voltagePhaseLineToNeutralMeasurandValue: number | undefined;
78085c42 169 if (voltagePhaseLineToNeutralSampledValueTemplate) {
5a47f72c
JB
170 const voltagePhaseLineToNeutralSampledValueTemplateValue = isNotEmptyString(
171 voltagePhaseLineToNeutralSampledValueTemplate.value,
172 )
173 ? parseInt(voltagePhaseLineToNeutralSampledValueTemplate.value)
174 : chargingStation.stationInfo.voltageOut!;
78085c42
JB
175 const fluctuationPhaseToNeutralPercent =
176 voltagePhaseLineToNeutralSampledValueTemplate.fluctuationPercent ??
177 Constants.DEFAULT_FLUCTUATION_PERCENT;
9bf0ef23 178 voltagePhaseLineToNeutralMeasurandValue = getRandomFloatFluctuatedRounded(
78085c42 179 voltagePhaseLineToNeutralSampledValueTemplateValue,
5edd8ba0 180 fluctuationPhaseToNeutralPercent,
78085c42
JB
181 );
182 }
183 meterValue.sampledValue.push(
184 OCPP16ServiceUtils.buildSampledValue(
185 voltagePhaseLineToNeutralSampledValueTemplate ?? voltageSampledValueTemplate,
186 voltagePhaseLineToNeutralMeasurandValue ?? voltageMeasurandValue,
72092cfc 187 undefined,
5edd8ba0
JB
188 phaseLineToNeutralValue as OCPP16MeterValuePhase,
189 ),
78085c42 190 );
5398cecf 191 if (chargingStation.stationInfo?.phaseLineToLineVoltageMeterValues) {
78085c42
JB
192 const phaseLineToLineValue = `L${phase}-L${
193 (phase + 1) % chargingStation.getNumberOfPhases() !== 0
194 ? (phase + 1) % chargingStation.getNumberOfPhases()
195 : chargingStation.getNumberOfPhases()
196 }`;
4c149643
JB
197 const voltagePhaseLineToLineValueRounded = roundTo(
198 Math.sqrt(chargingStation.getNumberOfPhases()) *
199 chargingStation.stationInfo.voltageOut!,
200 2,
201 );
78085c42 202 const voltagePhaseLineToLineSampledValueTemplate =
ed3d2808 203 OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 204 chargingStation,
78085c42
JB
205 connectorId,
206 OCPP16MeterValueMeasurand.VOLTAGE,
5edd8ba0 207 phaseLineToLineValue as OCPP16MeterValuePhase,
78085c42 208 );
e1d9a0f4 209 let voltagePhaseLineToLineMeasurandValue: number | undefined;
78085c42 210 if (voltagePhaseLineToLineSampledValueTemplate) {
5a47f72c
JB
211 const voltagePhaseLineToLineSampledValueTemplateValue = isNotEmptyString(
212 voltagePhaseLineToLineSampledValueTemplate.value,
213 )
214 ? parseInt(voltagePhaseLineToLineSampledValueTemplate.value)
4c149643 215 : voltagePhaseLineToLineValueRounded;
78085c42
JB
216 const fluctuationPhaseLineToLinePercent =
217 voltagePhaseLineToLineSampledValueTemplate.fluctuationPercent ??
218 Constants.DEFAULT_FLUCTUATION_PERCENT;
9bf0ef23 219 voltagePhaseLineToLineMeasurandValue = getRandomFloatFluctuatedRounded(
78085c42 220 voltagePhaseLineToLineSampledValueTemplateValue,
5edd8ba0 221 fluctuationPhaseLineToLinePercent,
78085c42
JB
222 );
223 }
9bf0ef23 224 const defaultVoltagePhaseLineToLineMeasurandValue = getRandomFloatFluctuatedRounded(
4c149643 225 voltagePhaseLineToLineValueRounded,
5edd8ba0 226 fluctuationPercent,
78085c42
JB
227 );
228 meterValue.sampledValue.push(
229 OCPP16ServiceUtils.buildSampledValue(
230 voltagePhaseLineToLineSampledValueTemplate ?? voltageSampledValueTemplate,
231 voltagePhaseLineToLineMeasurandValue ?? defaultVoltagePhaseLineToLineMeasurandValue,
72092cfc 232 undefined,
5edd8ba0
JB
233 phaseLineToLineValue as OCPP16MeterValuePhase,
234 ),
78085c42
JB
235 );
236 }
237 }
238 }
239 // Power.Active.Import measurand
ed3d2808 240 const powerSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 241 chargingStation,
78085c42 242 connectorId,
5edd8ba0 243 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
78085c42 244 );
abe9e9dd 245 let powerPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {};
78085c42
JB
246 if (chargingStation.getNumberOfPhases() === 3) {
247 powerPerPhaseSampledValueTemplates = {
ed3d2808 248 L1: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 249 chargingStation,
78085c42
JB
250 connectorId,
251 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
5edd8ba0 252 OCPP16MeterValuePhase.L1_N,
78085c42 253 ),
ed3d2808 254 L2: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 255 chargingStation,
78085c42
JB
256 connectorId,
257 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
5edd8ba0 258 OCPP16MeterValuePhase.L2_N,
78085c42 259 ),
ed3d2808 260 L3: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 261 chargingStation,
78085c42
JB
262 connectorId,
263 OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT,
5edd8ba0 264 OCPP16MeterValuePhase.L3_N,
78085c42
JB
265 ),
266 };
267 }
268 if (powerSampledValueTemplate) {
269 OCPP16ServiceUtils.checkMeasurandPowerDivider(
270 chargingStation,
e1d9a0f4 271 powerSampledValueTemplate.measurand!,
78085c42 272 );
fc040c43 273 const errMsg = `MeterValues measurand ${
78085c42
JB
274 powerSampledValueTemplate.measurand ??
275 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
5398cecf 276 }: Unknown ${chargingStation.stationInfo?.currentOutType} currentOutType in template file ${
2484ac1e 277 chargingStation.templateFile
78085c42
JB
278 }, cannot calculate ${
279 powerSampledValueTemplate.measurand ??
280 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
281 } measurand value`;
e1d9a0f4 282 const powerMeasurandValues: MeasurandValues = {} as MeasurandValues;
78085c42 283 const unitDivider = powerSampledValueTemplate?.unit === MeterValueUnit.KILO_WATT ? 1000 : 1;
1b6498ba
JB
284 const connectorMaximumAvailablePower =
285 chargingStation.getConnectorMaximumAvailablePower(connectorId);
286 const connectorMaximumPower = Math.round(connectorMaximumAvailablePower);
ad8537a7 287 const connectorMaximumPowerPerPhase = Math.round(
5edd8ba0 288 connectorMaximumAvailablePower / chargingStation.getNumberOfPhases(),
78085c42 289 );
d71ce3fa 290 const connectorMinimumPower = Math.round(powerSampledValueTemplate.minimumValue ?? 0);
860ef183 291 const connectorMinimumPowerPerPhase = Math.round(
5edd8ba0 292 connectorMinimumPower / chargingStation.getNumberOfPhases(),
860ef183 293 );
5398cecf 294 switch (chargingStation.stationInfo?.currentOutType) {
78085c42
JB
295 case CurrentType.AC:
296 if (chargingStation.getNumberOfPhases() === 3) {
969c488d
JB
297 const defaultFluctuatedPowerPerPhase = isNotEmptyString(powerSampledValueTemplate.value)
298 ? getRandomFloatFluctuatedRounded(
299 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
300 powerSampledValueTemplate.value,
301 connectorMaximumPower / unitDivider,
302 connectorMinimumPower / unitDivider,
303 {
304 limitationEnabled:
305 chargingStation.stationInfo?.customValueLimitationMeterValues,
306 fallbackValue: connectorMinimumPower / unitDivider,
307 },
308 ) / chargingStation.getNumberOfPhases(),
309 powerSampledValueTemplate.fluctuationPercent ??
310 Constants.DEFAULT_FLUCTUATION_PERCENT,
311 )
312 : undefined;
313 const phase1FluctuatedValue = isNotEmptyString(
314 powerPerPhaseSampledValueTemplates.L1?.value,
315 )
316 ? getRandomFloatFluctuatedRounded(
317 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
318 powerPerPhaseSampledValueTemplates.L1?.value,
319 connectorMaximumPowerPerPhase / unitDivider,
320 connectorMinimumPowerPerPhase / unitDivider,
321 {
322 limitationEnabled:
323 chargingStation.stationInfo?.customValueLimitationMeterValues,
324 fallbackValue: connectorMinimumPowerPerPhase / unitDivider,
325 },
326 ),
327 powerPerPhaseSampledValueTemplates.L1?.fluctuationPercent ??
328 Constants.DEFAULT_FLUCTUATION_PERCENT,
329 )
330 : undefined;
331 const phase2FluctuatedValue = isNotEmptyString(
332 powerPerPhaseSampledValueTemplates.L2?.value,
333 )
334 ? getRandomFloatFluctuatedRounded(
335 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
336 powerPerPhaseSampledValueTemplates.L2?.value,
337 connectorMaximumPowerPerPhase / unitDivider,
338 connectorMinimumPowerPerPhase / unitDivider,
339 {
340 limitationEnabled:
341 chargingStation.stationInfo?.customValueLimitationMeterValues,
342 fallbackValue: connectorMinimumPowerPerPhase / unitDivider,
343 },
344 ),
345 powerPerPhaseSampledValueTemplates.L2?.fluctuationPercent ??
346 Constants.DEFAULT_FLUCTUATION_PERCENT,
347 )
348 : undefined;
349 const phase3FluctuatedValue = isNotEmptyString(
350 powerPerPhaseSampledValueTemplates.L3?.value,
351 )
352 ? getRandomFloatFluctuatedRounded(
353 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
354 powerPerPhaseSampledValueTemplates.L3?.value,
355 connectorMaximumPowerPerPhase / unitDivider,
356 connectorMinimumPowerPerPhase / unitDivider,
357 {
358 limitationEnabled:
359 chargingStation.stationInfo?.customValueLimitationMeterValues,
360 fallbackValue: connectorMinimumPowerPerPhase / unitDivider,
361 },
362 ),
363 powerPerPhaseSampledValueTemplates.L3?.fluctuationPercent ??
364 Constants.DEFAULT_FLUCTUATION_PERCENT,
365 )
366 : undefined;
78085c42 367 powerMeasurandValues.L1 =
969c488d
JB
368 phase1FluctuatedValue ??
369 defaultFluctuatedPowerPerPhase ??
9bf0ef23 370 getRandomFloatRounded(
860ef183 371 connectorMaximumPowerPerPhase / unitDivider,
5edd8ba0 372 connectorMinimumPowerPerPhase / unitDivider,
860ef183 373 );
78085c42 374 powerMeasurandValues.L2 =
969c488d
JB
375 phase2FluctuatedValue ??
376 defaultFluctuatedPowerPerPhase ??
9bf0ef23 377 getRandomFloatRounded(
860ef183 378 connectorMaximumPowerPerPhase / unitDivider,
5edd8ba0 379 connectorMinimumPowerPerPhase / unitDivider,
860ef183 380 );
78085c42 381 powerMeasurandValues.L3 =
969c488d
JB
382 phase3FluctuatedValue ??
383 defaultFluctuatedPowerPerPhase ??
9bf0ef23 384 getRandomFloatRounded(
860ef183 385 connectorMaximumPowerPerPhase / unitDivider,
5edd8ba0 386 connectorMinimumPowerPerPhase / unitDivider,
860ef183 387 );
78085c42 388 } else {
856e8f67 389 powerMeasurandValues.L1 = isNotEmptyString(powerSampledValueTemplate.value)
9bf0ef23 390 ? getRandomFloatFluctuatedRounded(
7bc31f9c
JB
391 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
392 powerSampledValueTemplate.value,
393 connectorMaximumPower / unitDivider,
d71ce3fa 394 connectorMinimumPower / unitDivider,
5398cecf
JB
395 {
396 limitationEnabled:
397 chargingStation.stationInfo?.customValueLimitationMeterValues,
d71ce3fa 398 fallbackValue: connectorMinimumPower / unitDivider,
5398cecf 399 },
34464008 400 ),
78085c42 401 powerSampledValueTemplate.fluctuationPercent ??
5edd8ba0 402 Constants.DEFAULT_FLUCTUATION_PERCENT,
78085c42 403 )
9bf0ef23 404 : getRandomFloatRounded(
860ef183 405 connectorMaximumPower / unitDivider,
5edd8ba0 406 connectorMinimumPower / unitDivider,
860ef183 407 );
78085c42
JB
408 powerMeasurandValues.L2 = 0;
409 powerMeasurandValues.L3 = 0;
410 }
9bf0ef23 411 powerMeasurandValues.allPhases = roundTo(
78085c42 412 powerMeasurandValues.L1 + powerMeasurandValues.L2 + powerMeasurandValues.L3,
5edd8ba0 413 2,
78085c42
JB
414 );
415 break;
416 case CurrentType.DC:
5a47f72c 417 powerMeasurandValues.allPhases = isNotEmptyString(powerSampledValueTemplate.value)
9bf0ef23 418 ? getRandomFloatFluctuatedRounded(
7bc31f9c
JB
419 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
420 powerSampledValueTemplate.value,
421 connectorMaximumPower / unitDivider,
d71ce3fa 422 connectorMinimumPower / unitDivider,
5398cecf
JB
423 {
424 limitationEnabled:
425 chargingStation.stationInfo?.customValueLimitationMeterValues,
d71ce3fa 426 fallbackValue: connectorMinimumPower / unitDivider,
5398cecf 427 },
34464008 428 ),
78085c42 429 powerSampledValueTemplate.fluctuationPercent ??
5edd8ba0 430 Constants.DEFAULT_FLUCTUATION_PERCENT,
78085c42 431 )
9bf0ef23 432 : getRandomFloatRounded(
860ef183 433 connectorMaximumPower / unitDivider,
5edd8ba0 434 connectorMinimumPower / unitDivider,
860ef183 435 );
78085c42
JB
436 break;
437 default:
fc040c43 438 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
78085c42
JB
439 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
440 }
441 meterValue.sampledValue.push(
442 OCPP16ServiceUtils.buildSampledValue(
443 powerSampledValueTemplate,
5edd8ba0
JB
444 powerMeasurandValues.allPhases,
445 ),
78085c42
JB
446 );
447 const sampledValuesIndex = meterValue.sampledValue.length - 1;
9bf0ef23
JB
448 const connectorMaximumPowerRounded = roundTo(connectorMaximumPower / unitDivider, 2);
449 const connectorMinimumPowerRounded = roundTo(connectorMinimumPower / unitDivider, 2);
78085c42 450 if (
9bf0ef23 451 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
ad8537a7 452 connectorMaximumPowerRounded ||
9bf0ef23 453 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) <
860ef183 454 connectorMinimumPowerRounded ||
78085c42
JB
455 debug
456 ) {
457 logger.error(
458 `${chargingStation.logPrefix()} MeterValues measurand ${
459 meterValue.sampledValue[sampledValuesIndex].measurand ??
460 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
5edd8ba0 461 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumPowerRounded}/${
78085c42 462 meterValue.sampledValue[sampledValuesIndex].value
5edd8ba0 463 }/${connectorMaximumPowerRounded}`,
78085c42
JB
464 );
465 }
466 for (
467 let phase = 1;
468 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
469 phase++
470 ) {
471 const phaseValue = `L${phase}-N`;
472 meterValue.sampledValue.push(
473 OCPP16ServiceUtils.buildSampledValue(
a37fc6dc
JB
474 powerPerPhaseSampledValueTemplates[
475 `L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates
969c488d 476 ] ?? powerSampledValueTemplate,
a37fc6dc 477 powerMeasurandValues[`L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates],
72092cfc 478 undefined,
5edd8ba0
JB
479 phaseValue as OCPP16MeterValuePhase,
480 ),
78085c42
JB
481 );
482 const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1;
9bf0ef23 483 const connectorMaximumPowerPerPhaseRounded = roundTo(
ad8537a7 484 connectorMaximumPowerPerPhase / unitDivider,
5edd8ba0 485 2,
ad8537a7 486 );
9bf0ef23 487 const connectorMinimumPowerPerPhaseRounded = roundTo(
860ef183 488 connectorMinimumPowerPerPhase / unitDivider,
5edd8ba0 489 2,
860ef183 490 );
78085c42 491 if (
9bf0ef23 492 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
ad8537a7 493 connectorMaximumPowerPerPhaseRounded ||
9bf0ef23 494 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
860ef183 495 connectorMinimumPowerPerPhaseRounded ||
78085c42
JB
496 debug
497 ) {
498 logger.error(
499 `${chargingStation.logPrefix()} MeterValues measurand ${
500 meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ??
501 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
502 }: phase ${
503 meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
5edd8ba0 504 }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumPowerPerPhaseRounded}/${
78085c42 505 meterValue.sampledValue[sampledValuesPerPhaseIndex].value
5edd8ba0 506 }/${connectorMaximumPowerPerPhaseRounded}`,
78085c42
JB
507 );
508 }
509 }
510 }
511 // Current.Import measurand
ed3d2808 512 const currentSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 513 chargingStation,
78085c42 514 connectorId,
5edd8ba0 515 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
78085c42 516 );
abe9e9dd 517 let currentPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {};
78085c42
JB
518 if (chargingStation.getNumberOfPhases() === 3) {
519 currentPerPhaseSampledValueTemplates = {
ed3d2808 520 L1: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 521 chargingStation,
78085c42
JB
522 connectorId,
523 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
5edd8ba0 524 OCPP16MeterValuePhase.L1,
78085c42 525 ),
ed3d2808 526 L2: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 527 chargingStation,
78085c42
JB
528 connectorId,
529 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
5edd8ba0 530 OCPP16MeterValuePhase.L2,
78085c42 531 ),
ed3d2808 532 L3: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 533 chargingStation,
78085c42
JB
534 connectorId,
535 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
5edd8ba0 536 OCPP16MeterValuePhase.L3,
78085c42
JB
537 ),
538 };
539 }
540 if (currentSampledValueTemplate) {
541 OCPP16ServiceUtils.checkMeasurandPowerDivider(
542 chargingStation,
e1d9a0f4 543 currentSampledValueTemplate.measurand!,
78085c42 544 );
fc040c43 545 const errMsg = `MeterValues measurand ${
78085c42
JB
546 currentSampledValueTemplate.measurand ??
547 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
5398cecf 548 }: Unknown ${chargingStation.stationInfo?.currentOutType} currentOutType in template file ${
2484ac1e 549 chargingStation.templateFile
78085c42
JB
550 }, cannot calculate ${
551 currentSampledValueTemplate.measurand ??
552 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
553 } measurand value`;
abe9e9dd 554 const currentMeasurandValues: MeasurandValues = {} as MeasurandValues;
1b6498ba
JB
555 const connectorMaximumAvailablePower =
556 chargingStation.getConnectorMaximumAvailablePower(connectorId);
860ef183 557 const connectorMinimumAmperage = currentSampledValueTemplate.minimumValue ?? 0;
ad8537a7 558 let connectorMaximumAmperage: number;
5398cecf 559 switch (chargingStation.stationInfo?.currentOutType) {
78085c42 560 case CurrentType.AC:
ad8537a7 561 connectorMaximumAmperage = ACElectricUtils.amperagePerPhaseFromPower(
78085c42 562 chargingStation.getNumberOfPhases(),
1b6498ba 563 connectorMaximumAvailablePower,
5398cecf 564 chargingStation.stationInfo.voltageOut!,
78085c42
JB
565 );
566 if (chargingStation.getNumberOfPhases() === 3) {
969c488d
JB
567 const defaultFluctuatedAmperagePerPhase = isNotEmptyString(
568 currentSampledValueTemplate.value,
569 )
570 ? getRandomFloatFluctuatedRounded(
571 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
572 currentSampledValueTemplate.value,
573 connectorMaximumAmperage,
574 connectorMinimumAmperage,
575 {
576 limitationEnabled:
577 chargingStation.stationInfo?.customValueLimitationMeterValues,
578 fallbackValue: connectorMinimumAmperage,
579 },
580 ),
581 currentSampledValueTemplate.fluctuationPercent ??
582 Constants.DEFAULT_FLUCTUATION_PERCENT,
583 )
584 : undefined;
585 const phase1FluctuatedValue = isNotEmptyString(
586 currentPerPhaseSampledValueTemplates.L1?.value,
587 )
588 ? getRandomFloatFluctuatedRounded(
589 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
590 currentPerPhaseSampledValueTemplates.L1?.value,
591 connectorMaximumAmperage,
592 connectorMinimumAmperage,
593 {
594 limitationEnabled:
595 chargingStation.stationInfo?.customValueLimitationMeterValues,
596 fallbackValue: connectorMinimumAmperage,
597 },
598 ),
599 currentPerPhaseSampledValueTemplates.L1?.fluctuationPercent ??
600 Constants.DEFAULT_FLUCTUATION_PERCENT,
601 )
602 : undefined;
603 const phase2FluctuatedValue = isNotEmptyString(
604 currentPerPhaseSampledValueTemplates.L2?.value,
605 )
606 ? getRandomFloatFluctuatedRounded(
607 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
608 currentPerPhaseSampledValueTemplates.L2?.value,
609 connectorMaximumAmperage,
610 connectorMinimumAmperage,
611 {
612 limitationEnabled:
613 chargingStation.stationInfo?.customValueLimitationMeterValues,
614 fallbackValue: connectorMinimumAmperage,
615 },
616 ),
617 currentPerPhaseSampledValueTemplates.L2?.fluctuationPercent ??
618 Constants.DEFAULT_FLUCTUATION_PERCENT,
619 )
620 : undefined;
621 const phase3FluctuatedValue = isNotEmptyString(
622 currentPerPhaseSampledValueTemplates.L3?.value,
623 )
624 ? getRandomFloatFluctuatedRounded(
625 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
626 currentPerPhaseSampledValueTemplates.L3?.value,
627 connectorMaximumAmperage,
628 connectorMinimumAmperage,
629 {
630 limitationEnabled:
631 chargingStation.stationInfo?.customValueLimitationMeterValues,
632 fallbackValue: connectorMinimumAmperage,
633 },
634 ),
635 currentPerPhaseSampledValueTemplates.L3?.fluctuationPercent ??
636 Constants.DEFAULT_FLUCTUATION_PERCENT,
637 )
638 : undefined;
78085c42 639 currentMeasurandValues.L1 =
969c488d
JB
640 phase1FluctuatedValue ??
641 defaultFluctuatedAmperagePerPhase ??
9bf0ef23 642 getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
78085c42 643 currentMeasurandValues.L2 =
969c488d
JB
644 phase2FluctuatedValue ??
645 defaultFluctuatedAmperagePerPhase ??
9bf0ef23 646 getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
78085c42 647 currentMeasurandValues.L3 =
969c488d
JB
648 phase3FluctuatedValue ??
649 defaultFluctuatedAmperagePerPhase ??
9bf0ef23 650 getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
78085c42 651 } else {
5a47f72c 652 currentMeasurandValues.L1 = isNotEmptyString(currentSampledValueTemplate.value)
9bf0ef23 653 ? getRandomFloatFluctuatedRounded(
7bc31f9c
JB
654 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
655 currentSampledValueTemplate.value,
656 connectorMaximumAmperage,
d71ce3fa 657 connectorMinimumAmperage,
5398cecf
JB
658 {
659 limitationEnabled:
660 chargingStation.stationInfo?.customValueLimitationMeterValues,
d71ce3fa 661 fallbackValue: connectorMinimumAmperage,
5398cecf 662 },
7bc31f9c 663 ),
78085c42 664 currentSampledValueTemplate.fluctuationPercent ??
5edd8ba0 665 Constants.DEFAULT_FLUCTUATION_PERCENT,
78085c42 666 )
9bf0ef23 667 : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
78085c42
JB
668 currentMeasurandValues.L2 = 0;
669 currentMeasurandValues.L3 = 0;
670 }
9bf0ef23 671 currentMeasurandValues.allPhases = roundTo(
78085c42
JB
672 (currentMeasurandValues.L1 + currentMeasurandValues.L2 + currentMeasurandValues.L3) /
673 chargingStation.getNumberOfPhases(),
5edd8ba0 674 2,
78085c42
JB
675 );
676 break;
677 case CurrentType.DC:
ad8537a7 678 connectorMaximumAmperage = DCElectricUtils.amperage(
1b6498ba 679 connectorMaximumAvailablePower,
5398cecf 680 chargingStation.stationInfo.voltageOut!,
78085c42 681 );
5a47f72c 682 currentMeasurandValues.allPhases = isNotEmptyString(currentSampledValueTemplate.value)
9bf0ef23 683 ? getRandomFloatFluctuatedRounded(
7bc31f9c
JB
684 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
685 currentSampledValueTemplate.value,
686 connectorMaximumAmperage,
d71ce3fa 687 connectorMinimumAmperage,
5398cecf
JB
688 {
689 limitationEnabled:
690 chargingStation.stationInfo?.customValueLimitationMeterValues,
d71ce3fa 691 fallbackValue: connectorMinimumAmperage,
5398cecf 692 },
7bc31f9c 693 ),
78085c42 694 currentSampledValueTemplate.fluctuationPercent ??
5edd8ba0 695 Constants.DEFAULT_FLUCTUATION_PERCENT,
78085c42 696 )
9bf0ef23 697 : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
78085c42
JB
698 break;
699 default:
fc040c43 700 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
78085c42
JB
701 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
702 }
703 meterValue.sampledValue.push(
704 OCPP16ServiceUtils.buildSampledValue(
705 currentSampledValueTemplate,
5edd8ba0
JB
706 currentMeasurandValues.allPhases,
707 ),
78085c42
JB
708 );
709 const sampledValuesIndex = meterValue.sampledValue.length - 1;
710 if (
9bf0ef23 711 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
ad8537a7 712 connectorMaximumAmperage ||
9bf0ef23 713 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) <
860ef183 714 connectorMinimumAmperage ||
78085c42
JB
715 debug
716 ) {
717 logger.error(
718 `${chargingStation.logPrefix()} MeterValues measurand ${
719 meterValue.sampledValue[sampledValuesIndex].measurand ??
720 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
5edd8ba0 721 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${
78085c42 722 meterValue.sampledValue[sampledValuesIndex].value
5edd8ba0 723 }/${connectorMaximumAmperage}`,
78085c42
JB
724 );
725 }
726 for (
727 let phase = 1;
728 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
729 phase++
730 ) {
731 const phaseValue = `L${phase}`;
732 meterValue.sampledValue.push(
733 OCPP16ServiceUtils.buildSampledValue(
a37fc6dc
JB
734 currentPerPhaseSampledValueTemplates[
735 phaseValue as keyof MeasurandPerPhaseSampledValueTemplates
969c488d 736 ] ?? currentSampledValueTemplate,
a37fc6dc 737 currentMeasurandValues[phaseValue as keyof MeasurandPerPhaseSampledValueTemplates],
72092cfc 738 undefined,
5edd8ba0
JB
739 phaseValue as OCPP16MeterValuePhase,
740 ),
78085c42
JB
741 );
742 const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1;
743 if (
9bf0ef23 744 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
ad8537a7 745 connectorMaximumAmperage ||
9bf0ef23 746 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
860ef183 747 connectorMinimumAmperage ||
78085c42
JB
748 debug
749 ) {
750 logger.error(
751 `${chargingStation.logPrefix()} MeterValues measurand ${
752 meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ??
753 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
754 }: phase ${
755 meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
5edd8ba0 756 }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${
78085c42 757 meterValue.sampledValue[sampledValuesPerPhaseIndex].value
5edd8ba0 758 }/${connectorMaximumAmperage}`,
78085c42
JB
759 );
760 }
761 }
762 }
763 // Energy.Active.Import.Register measurand (default)
ed3d2808 764 const energySampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 765 chargingStation,
5edd8ba0 766 connectorId,
492cf6ab 767 );
78085c42
JB
768 if (energySampledValueTemplate) {
769 OCPP16ServiceUtils.checkMeasurandPowerDivider(
770 chargingStation,
e1d9a0f4 771 energySampledValueTemplate.measurand!,
78085c42
JB
772 );
773 const unitDivider =
774 energySampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
1b6498ba
JB
775 const connectorMaximumAvailablePower =
776 chargingStation.getConnectorMaximumAvailablePower(connectorId);
9bf0ef23 777 const connectorMaximumEnergyRounded = roundTo(
1b6498ba 778 (connectorMaximumAvailablePower * interval) / (3600 * 1000),
5edd8ba0 779 2,
78085c42 780 );
d71ce3fa
JB
781 const connectorMinimumEnergyRounded = roundTo(
782 energySampledValueTemplate.minimumValue ?? 0,
783 2,
784 );
77684af8 785 const energyValueRounded = isNotEmptyString(energySampledValueTemplate.value)
c6dcc331 786 ? getRandomFloatFluctuatedRounded(
7bc31f9c
JB
787 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
788 energySampledValueTemplate.value,
789 connectorMaximumEnergyRounded,
d71ce3fa 790 connectorMinimumEnergyRounded,
7bc31f9c 791 {
5398cecf 792 limitationEnabled: chargingStation.stationInfo?.customValueLimitationMeterValues,
7bc31f9c 793 unitMultiplier: unitDivider,
d71ce3fa 794 fallbackValue: connectorMinimumEnergyRounded,
5edd8ba0 795 },
34464008 796 ),
5edd8ba0 797 energySampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT,
78085c42 798 )
d71ce3fa 799 : getRandomFloatRounded(connectorMaximumEnergyRounded, connectorMinimumEnergyRounded);
78085c42 800 // Persist previous value on connector
e1d9a0f4
JB
801 if (connector) {
802 if (
803 isNullOrUndefined(connector.energyActiveImportRegisterValue) === false &&
804 connector.energyActiveImportRegisterValue! >= 0 &&
805 isNullOrUndefined(connector.transactionEnergyActiveImportRegisterValue) === false &&
806 connector.transactionEnergyActiveImportRegisterValue! >= 0
807 ) {
808 connector.energyActiveImportRegisterValue! += energyValueRounded;
809 connector.transactionEnergyActiveImportRegisterValue! += energyValueRounded;
810 } else {
811 connector.energyActiveImportRegisterValue = 0;
812 connector.transactionEnergyActiveImportRegisterValue = 0;
813 }
78085c42
JB
814 }
815 meterValue.sampledValue.push(
816 OCPP16ServiceUtils.buildSampledValue(
817 energySampledValueTemplate,
9bf0ef23 818 roundTo(
78085c42
JB
819 chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) /
820 unitDivider,
5edd8ba0
JB
821 2,
822 ),
823 ),
78085c42
JB
824 );
825 const sampledValuesIndex = meterValue.sampledValue.length - 1;
d71ce3fa
JB
826 if (
827 energyValueRounded > connectorMaximumEnergyRounded ||
828 energyValueRounded < connectorMinimumEnergyRounded ||
829 debug
830 ) {
78085c42
JB
831 logger.error(
832 `${chargingStation.logPrefix()} MeterValues measurand ${
833 meterValue.sampledValue[sampledValuesIndex].measurand ??
834 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
d71ce3fa 835 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumEnergyRounded}/${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${interval}ms`,
78085c42
JB
836 );
837 }
838 }
839 return meterValue;
840 }
841
e7aeea18
JB
842 public static buildTransactionBeginMeterValue(
843 chargingStation: ChargingStation,
844 connectorId: number,
5edd8ba0 845 meterStart: number,
e7aeea18 846 ): OCPP16MeterValue {
fd0c36fa 847 const meterValue: OCPP16MeterValue = {
c38f0ced 848 timestamp: new Date(),
fd0c36fa
JB
849 sampledValue: [],
850 };
9ccca265 851 // Energy.Active.Import.Register measurand (default)
ed3d2808 852 const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 853 chargingStation,
5edd8ba0 854 connectorId,
492cf6ab 855 );
9ccca265 856 const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
e7aeea18
JB
857 meterValue.sampledValue.push(
858 OCPP16ServiceUtils.buildSampledValue(
e1d9a0f4 859 sampledValueTemplate!,
9bf0ef23 860 roundTo((meterStart ?? 0) / unitDivider, 4),
5edd8ba0
JB
861 MeterValueContext.TRANSACTION_BEGIN,
862 ),
e7aeea18 863 );
fd0c36fa
JB
864 return meterValue;
865 }
866
e7aeea18
JB
867 public static buildTransactionEndMeterValue(
868 chargingStation: ChargingStation,
869 connectorId: number,
5edd8ba0 870 meterStop: number,
e7aeea18 871 ): OCPP16MeterValue {
fd0c36fa 872 const meterValue: OCPP16MeterValue = {
c38f0ced 873 timestamp: new Date(),
fd0c36fa
JB
874 sampledValue: [],
875 };
9ccca265 876 // Energy.Active.Import.Register measurand (default)
ed3d2808 877 const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 878 chargingStation,
5edd8ba0 879 connectorId,
492cf6ab 880 );
9ccca265 881 const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
e7aeea18
JB
882 meterValue.sampledValue.push(
883 OCPP16ServiceUtils.buildSampledValue(
e1d9a0f4 884 sampledValueTemplate!,
9bf0ef23 885 roundTo((meterStop ?? 0) / unitDivider, 4),
5edd8ba0
JB
886 MeterValueContext.TRANSACTION_END,
887 ),
e7aeea18 888 );
fd0c36fa
JB
889 return meterValue;
890 }
891
e7aeea18
JB
892 public static buildTransactionDataMeterValues(
893 transactionBeginMeterValue: OCPP16MeterValue,
5edd8ba0 894 transactionEndMeterValue: OCPP16MeterValue,
e7aeea18 895 ): OCPP16MeterValue[] {
fd0c36fa
JB
896 const meterValues: OCPP16MeterValue[] = [];
897 meterValues.push(transactionBeginMeterValue);
898 meterValues.push(transactionEndMeterValue);
899 return meterValues;
900 }
7bc31f9c 901
d19b10a8
JB
902 public static remoteStopTransaction = async (
903 chargingStation: ChargingStation,
904 connectorId: number,
905 ): Promise<GenericResponse> => {
906 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
907 chargingStation,
908 connectorId,
909 OCPP16ChargePointStatus.Finishing,
910 );
911 const stopResponse = await chargingStation.stopTransactionOnConnector(
912 connectorId,
913 OCPP16StopTransactionReason.REMOTE,
914 );
915 if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
916 return OCPP16Constants.OCPP_RESPONSE_ACCEPTED;
917 }
918 return OCPP16Constants.OCPP_RESPONSE_REJECTED;
919 };
920
366f75f6
JB
921 public static changeAvailability = async (
922 chargingStation: ChargingStation,
225e32b0 923 connectorIds: number[],
366f75f6
JB
924 chargePointStatus: OCPP16ChargePointStatus,
925 availabilityType: OCPP16AvailabilityType,
926 ): Promise<OCPP16ChangeAvailabilityResponse> => {
225e32b0
JB
927 const responses: OCPP16ChangeAvailabilityResponse[] = [];
928 for (const connectorId of connectorIds) {
929 let response: OCPP16ChangeAvailabilityResponse =
930 OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
931 const connectorStatus = chargingStation.getConnectorStatus(connectorId)!;
932 if (connectorStatus?.transactionStarted === true) {
933 response = OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
934 }
935 connectorStatus.availability = availabilityType;
936 if (response === OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED) {
937 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
938 chargingStation,
939 connectorId,
940 chargePointStatus,
941 );
942 }
943 responses.push(response);
366f75f6 944 }
3b0ed034 945 if (responses.includes(OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED)) {
225e32b0 946 return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
366f75f6 947 }
225e32b0 948 return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
366f75f6
JB
949 };
950
ed3d2808
JB
951 public static setChargingProfile(
952 chargingStation: ChargingStation,
953 connectorId: number,
5edd8ba0 954 cp: OCPP16ChargingProfile,
ed3d2808 955 ): void {
9bf0ef23 956 if (isNullOrUndefined(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
ed3d2808 957 logger.error(
5edd8ba0 958 `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an uninitialized charging profiles array attribute, applying deferred initialization`,
ed3d2808 959 );
e1d9a0f4 960 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = [];
ed3d2808 961 }
72092cfc
JB
962 if (
963 Array.isArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles) === false
964 ) {
ed3d2808 965 logger.error(
bbb55ee4 966 `${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 deferred initialization`,
ed3d2808 967 );
e1d9a0f4 968 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = [];
ed3d2808
JB
969 }
970 let cpReplaced = false;
9bf0ef23 971 if (isNotEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
ed3d2808
JB
972 chargingStation
973 .getConnectorStatus(connectorId)
72092cfc 974 ?.chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
ed3d2808
JB
975 if (
976 chargingProfile.chargingProfileId === cp.chargingProfileId ||
977 (chargingProfile.stackLevel === cp.stackLevel &&
978 chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)
979 ) {
e1d9a0f4 980 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles![index] = cp;
ed3d2808
JB
981 cpReplaced = true;
982 }
983 });
984 }
72092cfc 985 !cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp);
ed3d2808
JB
986 }
987
73d87be1
JB
988 public static clearChargingProfiles = (
989 chargingStation: ChargingStation,
990 commandPayload: ClearChargingProfileRequest,
991 chargingProfiles: OCPP16ChargingProfile[] | undefined,
992 ): boolean => {
0d1f33ba 993 const { id, chargingProfilePurpose, stackLevel } = commandPayload;
73d87be1
JB
994 let clearedCP = false;
995 if (isNotEmptyArray(chargingProfiles)) {
996 chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
997 let clearCurrentCP = false;
0d1f33ba 998 if (chargingProfile.chargingProfileId === id) {
73d87be1
JB
999 clearCurrentCP = true;
1000 }
0d1f33ba 1001 if (!chargingProfilePurpose && chargingProfile.stackLevel === stackLevel) {
73d87be1
JB
1002 clearCurrentCP = true;
1003 }
0d1f33ba 1004 if (!stackLevel && chargingProfile.chargingProfilePurpose === chargingProfilePurpose) {
73d87be1
JB
1005 clearCurrentCP = true;
1006 }
1007 if (
0d1f33ba
JB
1008 chargingProfile.stackLevel === stackLevel &&
1009 chargingProfile.chargingProfilePurpose === chargingProfilePurpose
73d87be1
JB
1010 ) {
1011 clearCurrentCP = true;
1012 }
1013 if (clearCurrentCP) {
1014 chargingProfiles.splice(index, 1);
1015 logger.debug(
1016 `${chargingStation.logPrefix()} Matching charging profile(s) cleared: %j`,
1017 chargingProfile,
1018 );
1019 clearedCP = true;
1020 }
1021 });
1022 }
1023 return clearedCP;
1024 };
1025
ef9e3b33 1026 public static composeChargingSchedules = (
4abf6441
JB
1027 chargingScheduleHigher: OCPP16ChargingSchedule | undefined,
1028 chargingScheduleLower: OCPP16ChargingSchedule | undefined,
d632062f 1029 compositeInterval: Interval,
ef9e3b33 1030 ): OCPP16ChargingSchedule | undefined => {
4abf6441 1031 if (!chargingScheduleHigher && !chargingScheduleLower) {
ef9e3b33
JB
1032 return undefined;
1033 }
4abf6441 1034 if (chargingScheduleHigher && !chargingScheduleLower) {
d632062f 1035 return OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleHigher, compositeInterval);
ef9e3b33 1036 }
4abf6441 1037 if (!chargingScheduleHigher && chargingScheduleLower) {
d632062f 1038 return OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleLower, compositeInterval);
ef9e3b33 1039 }
4abf6441 1040 const compositeChargingScheduleHigher: OCPP16ChargingSchedule | undefined =
d632062f 1041 OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleHigher!, compositeInterval);
4abf6441 1042 const compositeChargingScheduleLower: OCPP16ChargingSchedule | undefined =
d632062f 1043 OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleLower!, compositeInterval);
4abf6441
JB
1044 const compositeChargingScheduleHigherInterval: Interval = {
1045 start: compositeChargingScheduleHigher!.startSchedule!,
ef9e3b33 1046 end: addSeconds(
4abf6441
JB
1047 compositeChargingScheduleHigher!.startSchedule!,
1048 compositeChargingScheduleHigher!.duration!,
ef9e3b33
JB
1049 ),
1050 };
4abf6441
JB
1051 const compositeChargingScheduleLowerInterval: Interval = {
1052 start: compositeChargingScheduleLower!.startSchedule!,
ef9e3b33 1053 end: addSeconds(
4abf6441
JB
1054 compositeChargingScheduleLower!.startSchedule!,
1055 compositeChargingScheduleLower!.duration!,
ef9e3b33
JB
1056 ),
1057 };
4abf6441
JB
1058 const higherFirst = isBefore(
1059 compositeChargingScheduleHigherInterval.start,
1060 compositeChargingScheduleLowerInterval.start,
1061 );
ef9e3b33
JB
1062 if (
1063 !areIntervalsOverlapping(
4abf6441
JB
1064 compositeChargingScheduleHigherInterval,
1065 compositeChargingScheduleLowerInterval,
ef9e3b33
JB
1066 )
1067 ) {
1068 return {
4abf6441
JB
1069 ...compositeChargingScheduleLower,
1070 ...compositeChargingScheduleHigher!,
1071 startSchedule: higherFirst
1072 ? (compositeChargingScheduleHigherInterval.start as Date)
1073 : (compositeChargingScheduleLowerInterval.start as Date),
1074 duration: higherFirst
1075 ? differenceInSeconds(
1076 compositeChargingScheduleLowerInterval.end,
1077 compositeChargingScheduleHigherInterval.start,
1078 )
1079 : differenceInSeconds(
1080 compositeChargingScheduleHigherInterval.end,
1081 compositeChargingScheduleLowerInterval.start,
1082 ),
1083 chargingSchedulePeriod: [
1084 ...compositeChargingScheduleHigher!.chargingSchedulePeriod.map((schedulePeriod) => {
1085 return {
1086 ...schedulePeriod,
1087 startPeriod: higherFirst
1088 ? 0
1089 : schedulePeriod.startPeriod +
1090 differenceInSeconds(
1091 compositeChargingScheduleHigherInterval.start,
1092 compositeChargingScheduleLowerInterval.start,
1093 ),
1094 };
1095 }),
1096 ...compositeChargingScheduleLower!.chargingSchedulePeriod.map((schedulePeriod) => {
1097 return {
1098 ...schedulePeriod,
1099 startPeriod: higherFirst
1100 ? schedulePeriod.startPeriod +
1101 differenceInSeconds(
1102 compositeChargingScheduleLowerInterval.start,
1103 compositeChargingScheduleHigherInterval.start,
1104 )
1105 : 0,
1106 };
1107 }),
1108 ].sort((a, b) => a.startPeriod - b.startPeriod),
ef9e3b33
JB
1109 };
1110 }
4abf6441
JB
1111 return {
1112 ...compositeChargingScheduleLower,
1113 ...compositeChargingScheduleHigher!,
1114 startSchedule: higherFirst
1115 ? (compositeChargingScheduleHigherInterval.start as Date)
1116 : (compositeChargingScheduleLowerInterval.start as Date),
1117 duration: higherFirst
1118 ? differenceInSeconds(
1119 compositeChargingScheduleLowerInterval.end,
1120 compositeChargingScheduleHigherInterval.start,
1121 )
1122 : differenceInSeconds(
1123 compositeChargingScheduleHigherInterval.end,
1124 compositeChargingScheduleLowerInterval.start,
1125 ),
1126 chargingSchedulePeriod: [
1127 ...compositeChargingScheduleHigher!.chargingSchedulePeriod.map((schedulePeriod) => {
1128 return {
1129 ...schedulePeriod,
1130 startPeriod: higherFirst
1131 ? 0
1132 : schedulePeriod.startPeriod +
1133 differenceInSeconds(
1134 compositeChargingScheduleHigherInterval.start,
1135 compositeChargingScheduleLowerInterval.start,
1136 ),
1137 };
1138 }),
1139 ...compositeChargingScheduleLower!.chargingSchedulePeriod
c4ab56ba 1140 .filter((schedulePeriod, index) => {
4abf6441
JB
1141 if (
1142 higherFirst &&
1143 isWithinInterval(
1144 addSeconds(
1145 compositeChargingScheduleLowerInterval.start,
1146 schedulePeriod.startPeriod,
1147 ),
1148 {
1149 start: compositeChargingScheduleLowerInterval.start,
1150 end: compositeChargingScheduleHigherInterval.end,
1151 },
1152 )
1153 ) {
1154 return false;
1155 }
c4ab56ba
JB
1156 if (
1157 higherFirst &&
1158 index < compositeChargingScheduleLower!.chargingSchedulePeriod.length - 1 &&
1159 !isWithinInterval(
1160 addSeconds(
1161 compositeChargingScheduleLowerInterval.start,
1162 schedulePeriod.startPeriod,
1163 ),
1164 {
1165 start: compositeChargingScheduleLowerInterval.start,
1166 end: compositeChargingScheduleHigherInterval.end,
1167 },
1168 ) &&
1169 isWithinInterval(
1170 addSeconds(
1171 compositeChargingScheduleLowerInterval.start,
1172 compositeChargingScheduleLower!.chargingSchedulePeriod[index + 1].startPeriod,
1173 ),
1174 {
1175 start: compositeChargingScheduleLowerInterval.start,
1176 end: compositeChargingScheduleHigherInterval.end,
1177 },
1178 )
1179 ) {
c4ab56ba
JB
1180 return false;
1181 }
4abf6441
JB
1182 if (
1183 !higherFirst &&
1184 isWithinInterval(
1185 addSeconds(
1186 compositeChargingScheduleLowerInterval.start,
1187 schedulePeriod.startPeriod,
1188 ),
1189 {
1190 start: compositeChargingScheduleHigherInterval.start,
1191 end: compositeChargingScheduleLowerInterval.end,
1192 },
1193 )
1194 ) {
1195 return false;
1196 }
1197 return true;
1198 })
0e14e1d4
JB
1199 .map((schedulePeriod, index) => {
1200 if (index === 0 && schedulePeriod.startPeriod !== 0) {
1201 schedulePeriod.startPeriod = 0;
1202 }
4abf6441
JB
1203 return {
1204 ...schedulePeriod,
1205 startPeriod: higherFirst
1206 ? schedulePeriod.startPeriod +
1207 differenceInSeconds(
1208 compositeChargingScheduleLowerInterval.start,
1209 compositeChargingScheduleHigherInterval.start,
1210 )
1211 : 0,
1212 };
1213 }),
1214 ].sort((a, b) => a.startPeriod - b.startPeriod),
1215 };
ef9e3b33
JB
1216 };
1217
90aceaf6
JB
1218 public static hasReservation = (
1219 chargingStation: ChargingStation,
1220 connectorId: number,
1221 idTag: string,
1222 ): boolean => {
1223 const connectorReservation = chargingStation.getReservationBy('connectorId', connectorId);
1224 const chargingStationReservation = chargingStation.getReservationBy('connectorId', 0);
1225 if (
1226 (chargingStation.getConnectorStatus(connectorId)?.status ===
1227 OCPP16ChargePointStatus.Reserved &&
1228 connectorReservation &&
56563a3c 1229 !hasReservationExpired(connectorReservation) &&
90aceaf6 1230 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
56563a3c 1231 connectorReservation?.idTag === idTag) ||
90aceaf6
JB
1232 (chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved &&
1233 chargingStationReservation &&
56563a3c
JB
1234 !hasReservationExpired(chargingStationReservation) &&
1235 chargingStationReservation?.idTag === idTag)
90aceaf6 1236 ) {
88499f52
JB
1237 logger.debug(
1238 `${chargingStation.logPrefix()} Connector id ${connectorId} has a valid reservation for idTag ${idTag}: %j`,
1239 connectorReservation ?? chargingStationReservation,
1240 );
56563a3c 1241 return true;
90aceaf6 1242 }
56563a3c 1243 return false;
90aceaf6
JB
1244 };
1245
1b271a54
JB
1246 public static parseJsonSchemaFile<T extends JsonType>(
1247 relativePath: string,
1248 moduleName?: string,
5edd8ba0 1249 methodName?: string,
1b271a54 1250 ): JSONSchemaType<T> {
7164966d 1251 return super.parseJsonSchemaFile<T>(
51022aa0 1252 relativePath,
1b271a54
JB
1253 OCPPVersion.VERSION_16,
1254 moduleName,
5edd8ba0 1255 methodName,
7164966d 1256 );
130783a7
JB
1257 }
1258
ef9e3b33
JB
1259 private static composeChargingSchedule = (
1260 chargingSchedule: OCPP16ChargingSchedule,
d632062f 1261 compositeInterval: Interval,
ef9e3b33
JB
1262 ): OCPP16ChargingSchedule | undefined => {
1263 const chargingScheduleInterval: Interval = {
1264 start: chargingSchedule.startSchedule!,
1265 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
1266 };
d632062f 1267 if (areIntervalsOverlapping(chargingScheduleInterval, compositeInterval)) {
ef9e3b33 1268 chargingSchedule.chargingSchedulePeriod.sort((a, b) => a.startPeriod - b.startPeriod);
d632062f 1269 if (isBefore(chargingScheduleInterval.start, compositeInterval.start)) {
ef9e3b33
JB
1270 return {
1271 ...chargingSchedule,
d632062f
JB
1272 startSchedule: compositeInterval.start as Date,
1273 duration: differenceInSeconds(
1274 chargingScheduleInterval.end,
1275 compositeInterval.start as Date,
1276 ),
0e14e1d4
JB
1277 chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod
1278 .filter((schedulePeriod, index) => {
ef9e3b33
JB
1279 if (
1280 isWithinInterval(
1281 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
d632062f 1282 compositeInterval,
ef9e3b33
JB
1283 )
1284 ) {
1285 return true;
1286 }
1287 if (
1288 index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
1289 !isWithinInterval(
1290 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod),
d632062f 1291 compositeInterval,
ef9e3b33
JB
1292 ) &&
1293 isWithinInterval(
1294 addSeconds(
1295 chargingScheduleInterval.start,
1296 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
1297 ),
d632062f 1298 compositeInterval,
ef9e3b33
JB
1299 )
1300 ) {
ef9e3b33
JB
1301 return true;
1302 }
1303 return false;
0e14e1d4
JB
1304 })
1305 .map((schedulePeriod, index) => {
1306 if (index === 0 && schedulePeriod.startPeriod !== 0) {
1307 schedulePeriod.startPeriod = 0;
1308 }
1309 return schedulePeriod;
1310 }),
ef9e3b33
JB
1311 };
1312 }
d632062f 1313 if (isAfter(chargingScheduleInterval.end, compositeInterval.end)) {
ef9e3b33
JB
1314 return {
1315 ...chargingSchedule,
d632062f
JB
1316 duration: differenceInSeconds(
1317 compositeInterval.end as Date,
1318 chargingScheduleInterval.start,
1319 ),
ef9e3b33
JB
1320 chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter((schedulePeriod) =>
1321 isWithinInterval(
1322 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
d632062f 1323 compositeInterval,
ef9e3b33
JB
1324 ),
1325 ),
1326 };
1327 }
1328 return chargingSchedule;
1329 }
1330 };
1331
7bc31f9c
JB
1332 private static buildSampledValue(
1333 sampledValueTemplate: SampledValueTemplate,
1334 value: number,
1335 context?: MeterValueContext,
5edd8ba0 1336 phase?: OCPP16MeterValuePhase,
7bc31f9c 1337 ): OCPP16SampledValue {
4ed03b6e 1338 const sampledValueContext = context ?? sampledValueTemplate?.context;
7bc31f9c
JB
1339 const sampledValueLocation =
1340 sampledValueTemplate?.location ??
e1d9a0f4 1341 OCPP16ServiceUtils.getMeasurandDefaultLocation(sampledValueTemplate.measurand!);
4ed03b6e 1342 const sampledValuePhase = phase ?? sampledValueTemplate?.phase;
7bc31f9c 1343 return {
9bf0ef23 1344 ...(!isNullOrUndefined(sampledValueTemplate.unit) && {
7bc31f9c
JB
1345 unit: sampledValueTemplate.unit,
1346 }),
9bf0ef23
JB
1347 ...(!isNullOrUndefined(sampledValueContext) && { context: sampledValueContext }),
1348 ...(!isNullOrUndefined(sampledValueTemplate.measurand) && {
7bc31f9c
JB
1349 measurand: sampledValueTemplate.measurand,
1350 }),
9bf0ef23 1351 ...(!isNullOrUndefined(sampledValueLocation) && { location: sampledValueLocation }),
1b2cddac 1352 ...(!isNullOrUndefined(value) && { value: value.toString() }),
9bf0ef23 1353 ...(!isNullOrUndefined(sampledValuePhase) && { phase: sampledValuePhase }),
e1d9a0f4 1354 } as OCPP16SampledValue;
7bc31f9c
JB
1355 }
1356
1357 private static checkMeasurandPowerDivider(
1358 chargingStation: ChargingStation,
5edd8ba0 1359 measurandType: OCPP16MeterValueMeasurand,
7bc31f9c 1360 ): void {
9bf0ef23 1361 if (isUndefined(chargingStation.powerDivider)) {
fc040c43 1362 const errMsg = `MeterValues measurand ${
7bc31f9c
JB
1363 measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1364 }: powerDivider is undefined`;
fc040c43 1365 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
7bc31f9c 1366 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
fa7bccf4 1367 } else if (chargingStation?.powerDivider <= 0) {
fc040c43 1368 const errMsg = `MeterValues measurand ${
7bc31f9c 1369 measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
fa7bccf4 1370 }: powerDivider have zero or below value ${chargingStation.powerDivider}`;
fc040c43 1371 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
7bc31f9c
JB
1372 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
1373 }
1374 }
1375
1376 private static getMeasurandDefaultLocation(
5edd8ba0 1377 measurandType: OCPP16MeterValueMeasurand,
7bc31f9c
JB
1378 ): MeterValueLocation | undefined {
1379 switch (measurandType) {
1380 case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
1381 return MeterValueLocation.EV;
1382 }
1383 }
1384
3b0ed034
JB
1385 // private static getMeasurandDefaultUnit(
1386 // measurandType: OCPP16MeterValueMeasurand,
1387 // ): MeterValueUnit | undefined {
1388 // switch (measurandType) {
1389 // case OCPP16MeterValueMeasurand.CURRENT_EXPORT:
1390 // case OCPP16MeterValueMeasurand.CURRENT_IMPORT:
1391 // case OCPP16MeterValueMeasurand.CURRENT_OFFERED:
1392 // return MeterValueUnit.AMP;
1393 // case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER:
1394 // case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER:
1395 // return MeterValueUnit.WATT_HOUR;
1396 // case OCPP16MeterValueMeasurand.POWER_ACTIVE_EXPORT:
1397 // case OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT:
1398 // case OCPP16MeterValueMeasurand.POWER_OFFERED:
1399 // return MeterValueUnit.WATT;
1400 // case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
1401 // return MeterValueUnit.PERCENT;
1402 // case OCPP16MeterValueMeasurand.VOLTAGE:
1403 // return MeterValueUnit.VOLT;
1404 // }
1405 // }
6ed92bc1 1406}