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