fix: fallback to minimum measurand value at meterValues generation
[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 );
e1d9a0f4 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) {
297 const defaultFluctuatedPowerPerPhase =
298 powerSampledValueTemplate.value &&
9bf0ef23 299 getRandomFloatFluctuatedRounded(
7bc31f9c
JB
300 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
301 powerSampledValueTemplate.value,
302 connectorMaximumPower / unitDivider,
5398cecf
JB
303 {
304 limitationEnabled:
305 chargingStation.stationInfo?.customValueLimitationMeterValues,
5bb45fe6 306 defaultValue: connectorMinimumPower / unitDivider,
5398cecf 307 },
34464008 308 ) / chargingStation.getNumberOfPhases(),
78085c42 309 powerSampledValueTemplate.fluctuationPercent ??
5edd8ba0 310 Constants.DEFAULT_FLUCTUATION_PERCENT,
78085c42
JB
311 );
312 const phase1FluctuatedValue =
e1d9a0f4 313 powerPerPhaseSampledValueTemplates.L1?.value &&
9bf0ef23 314 getRandomFloatFluctuatedRounded(
7bc31f9c
JB
315 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
316 powerPerPhaseSampledValueTemplates.L1.value,
317 connectorMaximumPowerPerPhase / unitDivider,
5398cecf
JB
318 {
319 limitationEnabled:
320 chargingStation.stationInfo?.customValueLimitationMeterValues,
5bb45fe6 321 defaultValue: connectorMinimumPowerPerPhase / unitDivider,
5398cecf 322 },
34464008 323 ),
78085c42 324 powerPerPhaseSampledValueTemplates.L1.fluctuationPercent ??
5edd8ba0 325 Constants.DEFAULT_FLUCTUATION_PERCENT,
78085c42
JB
326 );
327 const phase2FluctuatedValue =
e1d9a0f4 328 powerPerPhaseSampledValueTemplates.L2?.value &&
9bf0ef23 329 getRandomFloatFluctuatedRounded(
7bc31f9c
JB
330 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
331 powerPerPhaseSampledValueTemplates.L2.value,
332 connectorMaximumPowerPerPhase / unitDivider,
5398cecf
JB
333 {
334 limitationEnabled:
335 chargingStation.stationInfo?.customValueLimitationMeterValues,
5bb45fe6 336 defaultValue: connectorMinimumPowerPerPhase / unitDivider,
5398cecf 337 },
34464008 338 ),
78085c42 339 powerPerPhaseSampledValueTemplates.L2.fluctuationPercent ??
5edd8ba0 340 Constants.DEFAULT_FLUCTUATION_PERCENT,
78085c42
JB
341 );
342 const phase3FluctuatedValue =
e1d9a0f4 343 powerPerPhaseSampledValueTemplates.L3?.value &&
9bf0ef23 344 getRandomFloatFluctuatedRounded(
7bc31f9c
JB
345 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
346 powerPerPhaseSampledValueTemplates.L3.value,
347 connectorMaximumPowerPerPhase / unitDivider,
5398cecf
JB
348 {
349 limitationEnabled:
350 chargingStation.stationInfo?.customValueLimitationMeterValues,
5bb45fe6 351 defaultValue: connectorMinimumPowerPerPhase / unitDivider,
5398cecf 352 },
34464008 353 ),
78085c42 354 powerPerPhaseSampledValueTemplates.L3.fluctuationPercent ??
5edd8ba0 355 Constants.DEFAULT_FLUCTUATION_PERCENT,
78085c42
JB
356 );
357 powerMeasurandValues.L1 =
e1d9a0f4
JB
358 (phase1FluctuatedValue as number) ??
359 (defaultFluctuatedPowerPerPhase as number) ??
9bf0ef23 360 getRandomFloatRounded(
860ef183 361 connectorMaximumPowerPerPhase / unitDivider,
5edd8ba0 362 connectorMinimumPowerPerPhase / unitDivider,
860ef183 363 );
78085c42 364 powerMeasurandValues.L2 =
e1d9a0f4
JB
365 (phase2FluctuatedValue as number) ??
366 (defaultFluctuatedPowerPerPhase as number) ??
9bf0ef23 367 getRandomFloatRounded(
860ef183 368 connectorMaximumPowerPerPhase / unitDivider,
5edd8ba0 369 connectorMinimumPowerPerPhase / unitDivider,
860ef183 370 );
78085c42 371 powerMeasurandValues.L3 =
e1d9a0f4
JB
372 (phase3FluctuatedValue as number) ??
373 (defaultFluctuatedPowerPerPhase as number) ??
9bf0ef23 374 getRandomFloatRounded(
860ef183 375 connectorMaximumPowerPerPhase / unitDivider,
5edd8ba0 376 connectorMinimumPowerPerPhase / unitDivider,
860ef183 377 );
78085c42 378 } else {
856e8f67 379 powerMeasurandValues.L1 = isNotEmptyString(powerSampledValueTemplate.value)
9bf0ef23 380 ? getRandomFloatFluctuatedRounded(
7bc31f9c
JB
381 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
382 powerSampledValueTemplate.value,
383 connectorMaximumPower / unitDivider,
5398cecf
JB
384 {
385 limitationEnabled:
386 chargingStation.stationInfo?.customValueLimitationMeterValues,
5bb45fe6 387 defaultValue: connectorMinimumPower / unitDivider,
5398cecf 388 },
34464008 389 ),
78085c42 390 powerSampledValueTemplate.fluctuationPercent ??
5edd8ba0 391 Constants.DEFAULT_FLUCTUATION_PERCENT,
78085c42 392 )
9bf0ef23 393 : getRandomFloatRounded(
860ef183 394 connectorMaximumPower / unitDivider,
5edd8ba0 395 connectorMinimumPower / unitDivider,
860ef183 396 );
78085c42
JB
397 powerMeasurandValues.L2 = 0;
398 powerMeasurandValues.L3 = 0;
399 }
9bf0ef23 400 powerMeasurandValues.allPhases = roundTo(
78085c42 401 powerMeasurandValues.L1 + powerMeasurandValues.L2 + powerMeasurandValues.L3,
5edd8ba0 402 2,
78085c42
JB
403 );
404 break;
405 case CurrentType.DC:
5a47f72c 406 powerMeasurandValues.allPhases = isNotEmptyString(powerSampledValueTemplate.value)
9bf0ef23 407 ? getRandomFloatFluctuatedRounded(
7bc31f9c
JB
408 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
409 powerSampledValueTemplate.value,
410 connectorMaximumPower / unitDivider,
5398cecf
JB
411 {
412 limitationEnabled:
413 chargingStation.stationInfo?.customValueLimitationMeterValues,
5bb45fe6 414 defaultValue: connectorMinimumPower / unitDivider,
5398cecf 415 },
34464008 416 ),
78085c42 417 powerSampledValueTemplate.fluctuationPercent ??
5edd8ba0 418 Constants.DEFAULT_FLUCTUATION_PERCENT,
78085c42 419 )
9bf0ef23 420 : getRandomFloatRounded(
860ef183 421 connectorMaximumPower / unitDivider,
5edd8ba0 422 connectorMinimumPower / unitDivider,
860ef183 423 );
78085c42
JB
424 break;
425 default:
fc040c43 426 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
78085c42
JB
427 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
428 }
429 meterValue.sampledValue.push(
430 OCPP16ServiceUtils.buildSampledValue(
431 powerSampledValueTemplate,
5edd8ba0
JB
432 powerMeasurandValues.allPhases,
433 ),
78085c42
JB
434 );
435 const sampledValuesIndex = meterValue.sampledValue.length - 1;
9bf0ef23
JB
436 const connectorMaximumPowerRounded = roundTo(connectorMaximumPower / unitDivider, 2);
437 const connectorMinimumPowerRounded = roundTo(connectorMinimumPower / unitDivider, 2);
78085c42 438 if (
9bf0ef23 439 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
ad8537a7 440 connectorMaximumPowerRounded ||
9bf0ef23 441 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) <
860ef183 442 connectorMinimumPowerRounded ||
78085c42
JB
443 debug
444 ) {
445 logger.error(
446 `${chargingStation.logPrefix()} MeterValues measurand ${
447 meterValue.sampledValue[sampledValuesIndex].measurand ??
448 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
5edd8ba0 449 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumPowerRounded}/${
78085c42 450 meterValue.sampledValue[sampledValuesIndex].value
5edd8ba0 451 }/${connectorMaximumPowerRounded}`,
78085c42
JB
452 );
453 }
454 for (
455 let phase = 1;
456 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
457 phase++
458 ) {
459 const phaseValue = `L${phase}-N`;
460 meterValue.sampledValue.push(
461 OCPP16ServiceUtils.buildSampledValue(
a37fc6dc
JB
462 powerPerPhaseSampledValueTemplates[
463 `L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates
464 ]! ?? powerSampledValueTemplate,
465 powerMeasurandValues[`L${phase}` as keyof MeasurandPerPhaseSampledValueTemplates],
72092cfc 466 undefined,
5edd8ba0
JB
467 phaseValue as OCPP16MeterValuePhase,
468 ),
78085c42
JB
469 );
470 const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1;
9bf0ef23 471 const connectorMaximumPowerPerPhaseRounded = roundTo(
ad8537a7 472 connectorMaximumPowerPerPhase / unitDivider,
5edd8ba0 473 2,
ad8537a7 474 );
9bf0ef23 475 const connectorMinimumPowerPerPhaseRounded = roundTo(
860ef183 476 connectorMinimumPowerPerPhase / unitDivider,
5edd8ba0 477 2,
860ef183 478 );
78085c42 479 if (
9bf0ef23 480 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
ad8537a7 481 connectorMaximumPowerPerPhaseRounded ||
9bf0ef23 482 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
860ef183 483 connectorMinimumPowerPerPhaseRounded ||
78085c42
JB
484 debug
485 ) {
486 logger.error(
487 `${chargingStation.logPrefix()} MeterValues measurand ${
488 meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ??
489 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
490 }: phase ${
491 meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
5edd8ba0 492 }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumPowerPerPhaseRounded}/${
78085c42 493 meterValue.sampledValue[sampledValuesPerPhaseIndex].value
5edd8ba0 494 }/${connectorMaximumPowerPerPhaseRounded}`,
78085c42
JB
495 );
496 }
497 }
498 }
499 // Current.Import measurand
ed3d2808 500 const currentSampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 501 chargingStation,
78085c42 502 connectorId,
5edd8ba0 503 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
78085c42 504 );
abe9e9dd 505 let currentPerPhaseSampledValueTemplates: MeasurandPerPhaseSampledValueTemplates = {};
78085c42
JB
506 if (chargingStation.getNumberOfPhases() === 3) {
507 currentPerPhaseSampledValueTemplates = {
ed3d2808 508 L1: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 509 chargingStation,
78085c42
JB
510 connectorId,
511 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
5edd8ba0 512 OCPP16MeterValuePhase.L1,
78085c42 513 ),
ed3d2808 514 L2: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 515 chargingStation,
78085c42
JB
516 connectorId,
517 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
5edd8ba0 518 OCPP16MeterValuePhase.L2,
78085c42 519 ),
ed3d2808 520 L3: OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 521 chargingStation,
78085c42
JB
522 connectorId,
523 OCPP16MeterValueMeasurand.CURRENT_IMPORT,
5edd8ba0 524 OCPP16MeterValuePhase.L3,
78085c42
JB
525 ),
526 };
527 }
528 if (currentSampledValueTemplate) {
529 OCPP16ServiceUtils.checkMeasurandPowerDivider(
530 chargingStation,
e1d9a0f4 531 currentSampledValueTemplate.measurand!,
78085c42 532 );
fc040c43 533 const errMsg = `MeterValues measurand ${
78085c42
JB
534 currentSampledValueTemplate.measurand ??
535 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
5398cecf 536 }: Unknown ${chargingStation.stationInfo?.currentOutType} currentOutType in template file ${
2484ac1e 537 chargingStation.templateFile
78085c42
JB
538 }, cannot calculate ${
539 currentSampledValueTemplate.measurand ??
540 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
541 } measurand value`;
abe9e9dd 542 const currentMeasurandValues: MeasurandValues = {} as MeasurandValues;
1b6498ba
JB
543 const connectorMaximumAvailablePower =
544 chargingStation.getConnectorMaximumAvailablePower(connectorId);
860ef183 545 const connectorMinimumAmperage = currentSampledValueTemplate.minimumValue ?? 0;
ad8537a7 546 let connectorMaximumAmperage: number;
5398cecf 547 switch (chargingStation.stationInfo?.currentOutType) {
78085c42 548 case CurrentType.AC:
ad8537a7 549 connectorMaximumAmperage = ACElectricUtils.amperagePerPhaseFromPower(
78085c42 550 chargingStation.getNumberOfPhases(),
1b6498ba 551 connectorMaximumAvailablePower,
5398cecf 552 chargingStation.stationInfo.voltageOut!,
78085c42
JB
553 );
554 if (chargingStation.getNumberOfPhases() === 3) {
555 const defaultFluctuatedAmperagePerPhase =
556 currentSampledValueTemplate.value &&
9bf0ef23 557 getRandomFloatFluctuatedRounded(
7bc31f9c
JB
558 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
559 currentSampledValueTemplate.value,
560 connectorMaximumAmperage,
5398cecf
JB
561 {
562 limitationEnabled:
563 chargingStation.stationInfo?.customValueLimitationMeterValues,
5bb45fe6 564 defaultValue: connectorMinimumAmperage,
5398cecf 565 },
7bc31f9c 566 ),
78085c42 567 currentSampledValueTemplate.fluctuationPercent ??
5edd8ba0 568 Constants.DEFAULT_FLUCTUATION_PERCENT,
78085c42
JB
569 );
570 const phase1FluctuatedValue =
e1d9a0f4 571 currentPerPhaseSampledValueTemplates.L1?.value &&
9bf0ef23 572 getRandomFloatFluctuatedRounded(
7bc31f9c
JB
573 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
574 currentPerPhaseSampledValueTemplates.L1.value,
575 connectorMaximumAmperage,
5398cecf
JB
576 {
577 limitationEnabled:
578 chargingStation.stationInfo?.customValueLimitationMeterValues,
5bb45fe6 579 defaultValue: connectorMinimumAmperage,
5398cecf 580 },
34464008 581 ),
78085c42 582 currentPerPhaseSampledValueTemplates.L1.fluctuationPercent ??
5edd8ba0 583 Constants.DEFAULT_FLUCTUATION_PERCENT,
78085c42
JB
584 );
585 const phase2FluctuatedValue =
e1d9a0f4 586 currentPerPhaseSampledValueTemplates.L2?.value &&
9bf0ef23 587 getRandomFloatFluctuatedRounded(
7bc31f9c
JB
588 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
589 currentPerPhaseSampledValueTemplates.L2.value,
590 connectorMaximumAmperage,
5398cecf
JB
591 {
592 limitationEnabled:
593 chargingStation.stationInfo?.customValueLimitationMeterValues,
5bb45fe6 594 defaultValue: connectorMinimumAmperage,
5398cecf 595 },
34464008 596 ),
78085c42 597 currentPerPhaseSampledValueTemplates.L2.fluctuationPercent ??
5edd8ba0 598 Constants.DEFAULT_FLUCTUATION_PERCENT,
78085c42
JB
599 );
600 const phase3FluctuatedValue =
e1d9a0f4 601 currentPerPhaseSampledValueTemplates.L3?.value &&
9bf0ef23 602 getRandomFloatFluctuatedRounded(
7bc31f9c
JB
603 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
604 currentPerPhaseSampledValueTemplates.L3.value,
605 connectorMaximumAmperage,
5398cecf
JB
606 {
607 limitationEnabled:
608 chargingStation.stationInfo?.customValueLimitationMeterValues,
5bb45fe6 609 defaultValue: connectorMinimumAmperage,
5398cecf 610 },
34464008 611 ),
78085c42 612 currentPerPhaseSampledValueTemplates.L3.fluctuationPercent ??
5edd8ba0 613 Constants.DEFAULT_FLUCTUATION_PERCENT,
78085c42
JB
614 );
615 currentMeasurandValues.L1 =
e1d9a0f4
JB
616 (phase1FluctuatedValue as number) ??
617 (defaultFluctuatedAmperagePerPhase as number) ??
9bf0ef23 618 getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
78085c42 619 currentMeasurandValues.L2 =
e1d9a0f4
JB
620 (phase2FluctuatedValue as number) ??
621 (defaultFluctuatedAmperagePerPhase as number) ??
9bf0ef23 622 getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
78085c42 623 currentMeasurandValues.L3 =
e1d9a0f4
JB
624 (phase3FluctuatedValue as number) ??
625 (defaultFluctuatedAmperagePerPhase as number) ??
9bf0ef23 626 getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
78085c42 627 } else {
5a47f72c 628 currentMeasurandValues.L1 = isNotEmptyString(currentSampledValueTemplate.value)
9bf0ef23 629 ? getRandomFloatFluctuatedRounded(
7bc31f9c
JB
630 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
631 currentSampledValueTemplate.value,
632 connectorMaximumAmperage,
5398cecf
JB
633 {
634 limitationEnabled:
635 chargingStation.stationInfo?.customValueLimitationMeterValues,
5bb45fe6 636 defaultValue: connectorMinimumAmperage,
5398cecf 637 },
7bc31f9c 638 ),
78085c42 639 currentSampledValueTemplate.fluctuationPercent ??
5edd8ba0 640 Constants.DEFAULT_FLUCTUATION_PERCENT,
78085c42 641 )
9bf0ef23 642 : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
78085c42
JB
643 currentMeasurandValues.L2 = 0;
644 currentMeasurandValues.L3 = 0;
645 }
9bf0ef23 646 currentMeasurandValues.allPhases = roundTo(
78085c42
JB
647 (currentMeasurandValues.L1 + currentMeasurandValues.L2 + currentMeasurandValues.L3) /
648 chargingStation.getNumberOfPhases(),
5edd8ba0 649 2,
78085c42
JB
650 );
651 break;
652 case CurrentType.DC:
ad8537a7 653 connectorMaximumAmperage = DCElectricUtils.amperage(
1b6498ba 654 connectorMaximumAvailablePower,
5398cecf 655 chargingStation.stationInfo.voltageOut!,
78085c42 656 );
5a47f72c 657 currentMeasurandValues.allPhases = isNotEmptyString(currentSampledValueTemplate.value)
9bf0ef23 658 ? getRandomFloatFluctuatedRounded(
7bc31f9c
JB
659 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
660 currentSampledValueTemplate.value,
661 connectorMaximumAmperage,
5398cecf
JB
662 {
663 limitationEnabled:
664 chargingStation.stationInfo?.customValueLimitationMeterValues,
5bb45fe6 665 defaultValue: connectorMinimumAmperage,
5398cecf 666 },
7bc31f9c 667 ),
78085c42 668 currentSampledValueTemplate.fluctuationPercent ??
5edd8ba0 669 Constants.DEFAULT_FLUCTUATION_PERCENT,
78085c42 670 )
9bf0ef23 671 : getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
78085c42
JB
672 break;
673 default:
fc040c43 674 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
78085c42
JB
675 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
676 }
677 meterValue.sampledValue.push(
678 OCPP16ServiceUtils.buildSampledValue(
679 currentSampledValueTemplate,
5edd8ba0
JB
680 currentMeasurandValues.allPhases,
681 ),
78085c42
JB
682 );
683 const sampledValuesIndex = meterValue.sampledValue.length - 1;
684 if (
9bf0ef23 685 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
ad8537a7 686 connectorMaximumAmperage ||
9bf0ef23 687 convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) <
860ef183 688 connectorMinimumAmperage ||
78085c42
JB
689 debug
690 ) {
691 logger.error(
692 `${chargingStation.logPrefix()} MeterValues measurand ${
693 meterValue.sampledValue[sampledValuesIndex].measurand ??
694 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
5edd8ba0 695 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${
78085c42 696 meterValue.sampledValue[sampledValuesIndex].value
5edd8ba0 697 }/${connectorMaximumAmperage}`,
78085c42
JB
698 );
699 }
700 for (
701 let phase = 1;
702 chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
703 phase++
704 ) {
705 const phaseValue = `L${phase}`;
706 meterValue.sampledValue.push(
707 OCPP16ServiceUtils.buildSampledValue(
a37fc6dc
JB
708 currentPerPhaseSampledValueTemplates[
709 phaseValue as keyof MeasurandPerPhaseSampledValueTemplates
710 ]! ?? currentSampledValueTemplate,
711 currentMeasurandValues[phaseValue as keyof MeasurandPerPhaseSampledValueTemplates],
72092cfc 712 undefined,
5edd8ba0
JB
713 phaseValue as OCPP16MeterValuePhase,
714 ),
78085c42
JB
715 );
716 const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1;
717 if (
9bf0ef23 718 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
ad8537a7 719 connectorMaximumAmperage ||
9bf0ef23 720 convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
860ef183 721 connectorMinimumAmperage ||
78085c42
JB
722 debug
723 ) {
724 logger.error(
725 `${chargingStation.logPrefix()} MeterValues measurand ${
726 meterValue.sampledValue[sampledValuesPerPhaseIndex].measurand ??
727 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
728 }: phase ${
729 meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
5edd8ba0 730 }, connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${connectorMinimumAmperage}/${
78085c42 731 meterValue.sampledValue[sampledValuesPerPhaseIndex].value
5edd8ba0 732 }/${connectorMaximumAmperage}`,
78085c42
JB
733 );
734 }
735 }
736 }
737 // Energy.Active.Import.Register measurand (default)
ed3d2808 738 const energySampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 739 chargingStation,
5edd8ba0 740 connectorId,
492cf6ab 741 );
78085c42
JB
742 if (energySampledValueTemplate) {
743 OCPP16ServiceUtils.checkMeasurandPowerDivider(
744 chargingStation,
e1d9a0f4 745 energySampledValueTemplate.measurand!,
78085c42
JB
746 );
747 const unitDivider =
748 energySampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
1b6498ba
JB
749 const connectorMaximumAvailablePower =
750 chargingStation.getConnectorMaximumAvailablePower(connectorId);
9bf0ef23 751 const connectorMaximumEnergyRounded = roundTo(
1b6498ba 752 (connectorMaximumAvailablePower * interval) / (3600 * 1000),
5edd8ba0 753 2,
78085c42 754 );
77684af8 755 const energyValueRounded = isNotEmptyString(energySampledValueTemplate.value)
78085c42 756 ? // Cumulate the fluctuated value around the static one
9bf0ef23 757 getRandomFloatFluctuatedRounded(
7bc31f9c
JB
758 OCPP16ServiceUtils.getLimitFromSampledValueTemplateCustomValue(
759 energySampledValueTemplate.value,
760 connectorMaximumEnergyRounded,
761 {
5398cecf 762 limitationEnabled: chargingStation.stationInfo?.customValueLimitationMeterValues,
7bc31f9c 763 unitMultiplier: unitDivider,
5edd8ba0 764 },
34464008 765 ),
5edd8ba0 766 energySampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT,
78085c42 767 )
9bf0ef23 768 : getRandomFloatRounded(connectorMaximumEnergyRounded);
78085c42 769 // Persist previous value on connector
e1d9a0f4
JB
770 if (connector) {
771 if (
772 isNullOrUndefined(connector.energyActiveImportRegisterValue) === false &&
773 connector.energyActiveImportRegisterValue! >= 0 &&
774 isNullOrUndefined(connector.transactionEnergyActiveImportRegisterValue) === false &&
775 connector.transactionEnergyActiveImportRegisterValue! >= 0
776 ) {
777 connector.energyActiveImportRegisterValue! += energyValueRounded;
778 connector.transactionEnergyActiveImportRegisterValue! += energyValueRounded;
779 } else {
780 connector.energyActiveImportRegisterValue = 0;
781 connector.transactionEnergyActiveImportRegisterValue = 0;
782 }
78085c42
JB
783 }
784 meterValue.sampledValue.push(
785 OCPP16ServiceUtils.buildSampledValue(
786 energySampledValueTemplate,
9bf0ef23 787 roundTo(
78085c42
JB
788 chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId) /
789 unitDivider,
5edd8ba0
JB
790 2,
791 ),
792 ),
78085c42
JB
793 );
794 const sampledValuesIndex = meterValue.sampledValue.length - 1;
ad8537a7 795 if (energyValueRounded > connectorMaximumEnergyRounded || debug) {
78085c42
JB
796 logger.error(
797 `${chargingStation.logPrefix()} MeterValues measurand ${
798 meterValue.sampledValue[sampledValuesIndex].measurand ??
799 OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
04c32a95 800 }: connector id ${connectorId}, transaction id ${connector?.transactionId}, value: ${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${interval}ms`,
78085c42
JB
801 );
802 }
803 }
804 return meterValue;
805 }
806
e7aeea18
JB
807 public static buildTransactionBeginMeterValue(
808 chargingStation: ChargingStation,
809 connectorId: number,
5edd8ba0 810 meterStart: number,
e7aeea18 811 ): OCPP16MeterValue {
fd0c36fa 812 const meterValue: OCPP16MeterValue = {
c38f0ced 813 timestamp: new Date(),
fd0c36fa
JB
814 sampledValue: [],
815 };
9ccca265 816 // Energy.Active.Import.Register measurand (default)
ed3d2808 817 const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 818 chargingStation,
5edd8ba0 819 connectorId,
492cf6ab 820 );
9ccca265 821 const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
e7aeea18
JB
822 meterValue.sampledValue.push(
823 OCPP16ServiceUtils.buildSampledValue(
e1d9a0f4 824 sampledValueTemplate!,
9bf0ef23 825 roundTo((meterStart ?? 0) / unitDivider, 4),
5edd8ba0
JB
826 MeterValueContext.TRANSACTION_BEGIN,
827 ),
e7aeea18 828 );
fd0c36fa
JB
829 return meterValue;
830 }
831
e7aeea18
JB
832 public static buildTransactionEndMeterValue(
833 chargingStation: ChargingStation,
834 connectorId: number,
5edd8ba0 835 meterStop: number,
e7aeea18 836 ): OCPP16MeterValue {
fd0c36fa 837 const meterValue: OCPP16MeterValue = {
c38f0ced 838 timestamp: new Date(),
fd0c36fa
JB
839 sampledValue: [],
840 };
9ccca265 841 // Energy.Active.Import.Register measurand (default)
ed3d2808 842 const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 843 chargingStation,
5edd8ba0 844 connectorId,
492cf6ab 845 );
9ccca265 846 const unitDivider = sampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
e7aeea18
JB
847 meterValue.sampledValue.push(
848 OCPP16ServiceUtils.buildSampledValue(
e1d9a0f4 849 sampledValueTemplate!,
9bf0ef23 850 roundTo((meterStop ?? 0) / unitDivider, 4),
5edd8ba0
JB
851 MeterValueContext.TRANSACTION_END,
852 ),
e7aeea18 853 );
fd0c36fa
JB
854 return meterValue;
855 }
856
e7aeea18
JB
857 public static buildTransactionDataMeterValues(
858 transactionBeginMeterValue: OCPP16MeterValue,
5edd8ba0 859 transactionEndMeterValue: OCPP16MeterValue,
e7aeea18 860 ): OCPP16MeterValue[] {
fd0c36fa
JB
861 const meterValues: OCPP16MeterValue[] = [];
862 meterValues.push(transactionBeginMeterValue);
863 meterValues.push(transactionEndMeterValue);
864 return meterValues;
865 }
7bc31f9c 866
d19b10a8
JB
867 public static remoteStopTransaction = async (
868 chargingStation: ChargingStation,
869 connectorId: number,
870 ): Promise<GenericResponse> => {
871 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
872 chargingStation,
873 connectorId,
874 OCPP16ChargePointStatus.Finishing,
875 );
876 const stopResponse = await chargingStation.stopTransactionOnConnector(
877 connectorId,
878 OCPP16StopTransactionReason.REMOTE,
879 );
880 if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
881 return OCPP16Constants.OCPP_RESPONSE_ACCEPTED;
882 }
883 return OCPP16Constants.OCPP_RESPONSE_REJECTED;
884 };
885
366f75f6
JB
886 public static changeAvailability = async (
887 chargingStation: ChargingStation,
225e32b0 888 connectorIds: number[],
366f75f6
JB
889 chargePointStatus: OCPP16ChargePointStatus,
890 availabilityType: OCPP16AvailabilityType,
891 ): Promise<OCPP16ChangeAvailabilityResponse> => {
225e32b0
JB
892 const responses: OCPP16ChangeAvailabilityResponse[] = [];
893 for (const connectorId of connectorIds) {
894 let response: OCPP16ChangeAvailabilityResponse =
895 OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
896 const connectorStatus = chargingStation.getConnectorStatus(connectorId)!;
897 if (connectorStatus?.transactionStarted === true) {
898 response = OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
899 }
900 connectorStatus.availability = availabilityType;
901 if (response === OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED) {
902 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
903 chargingStation,
904 connectorId,
905 chargePointStatus,
906 );
907 }
908 responses.push(response);
366f75f6 909 }
3b0ed034 910 if (responses.includes(OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED)) {
225e32b0 911 return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
366f75f6 912 }
225e32b0 913 return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
366f75f6
JB
914 };
915
ed3d2808
JB
916 public static setChargingProfile(
917 chargingStation: ChargingStation,
918 connectorId: number,
5edd8ba0 919 cp: OCPP16ChargingProfile,
ed3d2808 920 ): void {
9bf0ef23 921 if (isNullOrUndefined(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
ed3d2808 922 logger.error(
5edd8ba0 923 `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an uninitialized charging profiles array attribute, applying deferred initialization`,
ed3d2808 924 );
e1d9a0f4 925 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = [];
ed3d2808 926 }
72092cfc
JB
927 if (
928 Array.isArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles) === false
929 ) {
ed3d2808 930 logger.error(
bbb55ee4 931 `${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 932 );
e1d9a0f4 933 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = [];
ed3d2808
JB
934 }
935 let cpReplaced = false;
9bf0ef23 936 if (isNotEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
ed3d2808
JB
937 chargingStation
938 .getConnectorStatus(connectorId)
72092cfc 939 ?.chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
ed3d2808
JB
940 if (
941 chargingProfile.chargingProfileId === cp.chargingProfileId ||
942 (chargingProfile.stackLevel === cp.stackLevel &&
943 chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)
944 ) {
e1d9a0f4 945 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles![index] = cp;
ed3d2808
JB
946 cpReplaced = true;
947 }
948 });
949 }
72092cfc 950 !cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp);
ed3d2808
JB
951 }
952
73d87be1
JB
953 public static clearChargingProfiles = (
954 chargingStation: ChargingStation,
955 commandPayload: ClearChargingProfileRequest,
956 chargingProfiles: OCPP16ChargingProfile[] | undefined,
957 ): boolean => {
0d1f33ba 958 const { id, chargingProfilePurpose, stackLevel } = commandPayload;
73d87be1
JB
959 let clearedCP = false;
960 if (isNotEmptyArray(chargingProfiles)) {
961 chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
962 let clearCurrentCP = false;
0d1f33ba 963 if (chargingProfile.chargingProfileId === id) {
73d87be1
JB
964 clearCurrentCP = true;
965 }
0d1f33ba 966 if (!chargingProfilePurpose && chargingProfile.stackLevel === stackLevel) {
73d87be1
JB
967 clearCurrentCP = true;
968 }
0d1f33ba 969 if (!stackLevel && chargingProfile.chargingProfilePurpose === chargingProfilePurpose) {
73d87be1
JB
970 clearCurrentCP = true;
971 }
972 if (
0d1f33ba
JB
973 chargingProfile.stackLevel === stackLevel &&
974 chargingProfile.chargingProfilePurpose === chargingProfilePurpose
73d87be1
JB
975 ) {
976 clearCurrentCP = true;
977 }
978 if (clearCurrentCP) {
979 chargingProfiles.splice(index, 1);
980 logger.debug(
981 `${chargingStation.logPrefix()} Matching charging profile(s) cleared: %j`,
982 chargingProfile,
983 );
984 clearedCP = true;
985 }
986 });
987 }
988 return clearedCP;
989 };
990
ef9e3b33 991 public static composeChargingSchedules = (
4abf6441
JB
992 chargingScheduleHigher: OCPP16ChargingSchedule | undefined,
993 chargingScheduleLower: OCPP16ChargingSchedule | undefined,
d632062f 994 compositeInterval: Interval,
ef9e3b33 995 ): OCPP16ChargingSchedule | undefined => {
4abf6441 996 if (!chargingScheduleHigher && !chargingScheduleLower) {
ef9e3b33
JB
997 return undefined;
998 }
4abf6441 999 if (chargingScheduleHigher && !chargingScheduleLower) {
d632062f 1000 return OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleHigher, compositeInterval);
ef9e3b33 1001 }
4abf6441 1002 if (!chargingScheduleHigher && chargingScheduleLower) {
d632062f 1003 return OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleLower, compositeInterval);
ef9e3b33 1004 }
4abf6441 1005 const compositeChargingScheduleHigher: OCPP16ChargingSchedule | undefined =
d632062f 1006 OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleHigher!, compositeInterval);
4abf6441 1007 const compositeChargingScheduleLower: OCPP16ChargingSchedule | undefined =
d632062f 1008 OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleLower!, compositeInterval);
4abf6441
JB
1009 const compositeChargingScheduleHigherInterval: Interval = {
1010 start: compositeChargingScheduleHigher!.startSchedule!,
ef9e3b33 1011 end: addSeconds(
4abf6441
JB
1012 compositeChargingScheduleHigher!.startSchedule!,
1013 compositeChargingScheduleHigher!.duration!,
ef9e3b33
JB
1014 ),
1015 };
4abf6441
JB
1016 const compositeChargingScheduleLowerInterval: Interval = {
1017 start: compositeChargingScheduleLower!.startSchedule!,
ef9e3b33 1018 end: addSeconds(
4abf6441
JB
1019 compositeChargingScheduleLower!.startSchedule!,
1020 compositeChargingScheduleLower!.duration!,
ef9e3b33
JB
1021 ),
1022 };
4abf6441
JB
1023 const higherFirst = isBefore(
1024 compositeChargingScheduleHigherInterval.start,
1025 compositeChargingScheduleLowerInterval.start,
1026 );
ef9e3b33
JB
1027 if (
1028 !areIntervalsOverlapping(
4abf6441
JB
1029 compositeChargingScheduleHigherInterval,
1030 compositeChargingScheduleLowerInterval,
ef9e3b33
JB
1031 )
1032 ) {
1033 return {
4abf6441
JB
1034 ...compositeChargingScheduleLower,
1035 ...compositeChargingScheduleHigher!,
1036 startSchedule: higherFirst
1037 ? (compositeChargingScheduleHigherInterval.start as Date)
1038 : (compositeChargingScheduleLowerInterval.start as Date),
1039 duration: higherFirst
1040 ? differenceInSeconds(
1041 compositeChargingScheduleLowerInterval.end,
1042 compositeChargingScheduleHigherInterval.start,
1043 )
1044 : differenceInSeconds(
1045 compositeChargingScheduleHigherInterval.end,
1046 compositeChargingScheduleLowerInterval.start,
1047 ),
1048 chargingSchedulePeriod: [
1049 ...compositeChargingScheduleHigher!.chargingSchedulePeriod.map((schedulePeriod) => {
1050 return {
1051 ...schedulePeriod,
1052 startPeriod: higherFirst
1053 ? 0
1054 : schedulePeriod.startPeriod +
1055 differenceInSeconds(
1056 compositeChargingScheduleHigherInterval.start,
1057 compositeChargingScheduleLowerInterval.start,
1058 ),
1059 };
1060 }),
1061 ...compositeChargingScheduleLower!.chargingSchedulePeriod.map((schedulePeriod) => {
1062 return {
1063 ...schedulePeriod,
1064 startPeriod: higherFirst
1065 ? schedulePeriod.startPeriod +
1066 differenceInSeconds(
1067 compositeChargingScheduleLowerInterval.start,
1068 compositeChargingScheduleHigherInterval.start,
1069 )
1070 : 0,
1071 };
1072 }),
1073 ].sort((a, b) => a.startPeriod - b.startPeriod),
ef9e3b33
JB
1074 };
1075 }
4abf6441
JB
1076 return {
1077 ...compositeChargingScheduleLower,
1078 ...compositeChargingScheduleHigher!,
1079 startSchedule: higherFirst
1080 ? (compositeChargingScheduleHigherInterval.start as Date)
1081 : (compositeChargingScheduleLowerInterval.start as Date),
1082 duration: higherFirst
1083 ? differenceInSeconds(
1084 compositeChargingScheduleLowerInterval.end,
1085 compositeChargingScheduleHigherInterval.start,
1086 )
1087 : differenceInSeconds(
1088 compositeChargingScheduleHigherInterval.end,
1089 compositeChargingScheduleLowerInterval.start,
1090 ),
1091 chargingSchedulePeriod: [
1092 ...compositeChargingScheduleHigher!.chargingSchedulePeriod.map((schedulePeriod) => {
1093 return {
1094 ...schedulePeriod,
1095 startPeriod: higherFirst
1096 ? 0
1097 : schedulePeriod.startPeriod +
1098 differenceInSeconds(
1099 compositeChargingScheduleHigherInterval.start,
1100 compositeChargingScheduleLowerInterval.start,
1101 ),
1102 };
1103 }),
1104 ...compositeChargingScheduleLower!.chargingSchedulePeriod
c4ab56ba 1105 .filter((schedulePeriod, index) => {
4abf6441
JB
1106 if (
1107 higherFirst &&
1108 isWithinInterval(
1109 addSeconds(
1110 compositeChargingScheduleLowerInterval.start,
1111 schedulePeriod.startPeriod,
1112 ),
1113 {
1114 start: compositeChargingScheduleLowerInterval.start,
1115 end: compositeChargingScheduleHigherInterval.end,
1116 },
1117 )
1118 ) {
1119 return false;
1120 }
c4ab56ba
JB
1121 if (
1122 higherFirst &&
1123 index < compositeChargingScheduleLower!.chargingSchedulePeriod.length - 1 &&
1124 !isWithinInterval(
1125 addSeconds(
1126 compositeChargingScheduleLowerInterval.start,
1127 schedulePeriod.startPeriod,
1128 ),
1129 {
1130 start: compositeChargingScheduleLowerInterval.start,
1131 end: compositeChargingScheduleHigherInterval.end,
1132 },
1133 ) &&
1134 isWithinInterval(
1135 addSeconds(
1136 compositeChargingScheduleLowerInterval.start,
1137 compositeChargingScheduleLower!.chargingSchedulePeriod[index + 1].startPeriod,
1138 ),
1139 {
1140 start: compositeChargingScheduleLowerInterval.start,
1141 end: compositeChargingScheduleHigherInterval.end,
1142 },
1143 )
1144 ) {
c4ab56ba
JB
1145 return false;
1146 }
4abf6441
JB
1147 if (
1148 !higherFirst &&
1149 isWithinInterval(
1150 addSeconds(
1151 compositeChargingScheduleLowerInterval.start,
1152 schedulePeriod.startPeriod,
1153 ),
1154 {
1155 start: compositeChargingScheduleHigherInterval.start,
1156 end: compositeChargingScheduleLowerInterval.end,
1157 },
1158 )
1159 ) {
1160 return false;
1161 }
1162 return true;
1163 })
0e14e1d4
JB
1164 .map((schedulePeriod, index) => {
1165 if (index === 0 && schedulePeriod.startPeriod !== 0) {
1166 schedulePeriod.startPeriod = 0;
1167 }
4abf6441
JB
1168 return {
1169 ...schedulePeriod,
1170 startPeriod: higherFirst
1171 ? schedulePeriod.startPeriod +
1172 differenceInSeconds(
1173 compositeChargingScheduleLowerInterval.start,
1174 compositeChargingScheduleHigherInterval.start,
1175 )
1176 : 0,
1177 };
1178 }),
1179 ].sort((a, b) => a.startPeriod - b.startPeriod),
1180 };
ef9e3b33
JB
1181 };
1182
90aceaf6
JB
1183 public static hasReservation = (
1184 chargingStation: ChargingStation,
1185 connectorId: number,
1186 idTag: string,
1187 ): boolean => {
1188 const connectorReservation = chargingStation.getReservationBy('connectorId', connectorId);
1189 const chargingStationReservation = chargingStation.getReservationBy('connectorId', 0);
1190 if (
1191 (chargingStation.getConnectorStatus(connectorId)?.status ===
1192 OCPP16ChargePointStatus.Reserved &&
1193 connectorReservation &&
56563a3c 1194 !hasReservationExpired(connectorReservation) &&
90aceaf6 1195 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
56563a3c 1196 connectorReservation?.idTag === idTag) ||
90aceaf6
JB
1197 (chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved &&
1198 chargingStationReservation &&
56563a3c
JB
1199 !hasReservationExpired(chargingStationReservation) &&
1200 chargingStationReservation?.idTag === idTag)
90aceaf6 1201 ) {
88499f52
JB
1202 logger.debug(
1203 `${chargingStation.logPrefix()} Connector id ${connectorId} has a valid reservation for idTag ${idTag}: %j`,
1204 connectorReservation ?? chargingStationReservation,
1205 );
56563a3c 1206 return true;
90aceaf6 1207 }
56563a3c 1208 return false;
90aceaf6
JB
1209 };
1210
1b271a54
JB
1211 public static parseJsonSchemaFile<T extends JsonType>(
1212 relativePath: string,
1213 moduleName?: string,
5edd8ba0 1214 methodName?: string,
1b271a54 1215 ): JSONSchemaType<T> {
7164966d 1216 return super.parseJsonSchemaFile<T>(
51022aa0 1217 relativePath,
1b271a54
JB
1218 OCPPVersion.VERSION_16,
1219 moduleName,
5edd8ba0 1220 methodName,
7164966d 1221 );
130783a7
JB
1222 }
1223
ef9e3b33
JB
1224 private static composeChargingSchedule = (
1225 chargingSchedule: OCPP16ChargingSchedule,
d632062f 1226 compositeInterval: Interval,
ef9e3b33
JB
1227 ): OCPP16ChargingSchedule | undefined => {
1228 const chargingScheduleInterval: Interval = {
1229 start: chargingSchedule.startSchedule!,
1230 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
1231 };
d632062f 1232 if (areIntervalsOverlapping(chargingScheduleInterval, compositeInterval)) {
ef9e3b33 1233 chargingSchedule.chargingSchedulePeriod.sort((a, b) => a.startPeriod - b.startPeriod);
d632062f 1234 if (isBefore(chargingScheduleInterval.start, compositeInterval.start)) {
ef9e3b33
JB
1235 return {
1236 ...chargingSchedule,
d632062f
JB
1237 startSchedule: compositeInterval.start as Date,
1238 duration: differenceInSeconds(
1239 chargingScheduleInterval.end,
1240 compositeInterval.start as Date,
1241 ),
0e14e1d4
JB
1242 chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod
1243 .filter((schedulePeriod, index) => {
ef9e3b33
JB
1244 if (
1245 isWithinInterval(
1246 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
d632062f 1247 compositeInterval,
ef9e3b33
JB
1248 )
1249 ) {
1250 return true;
1251 }
1252 if (
1253 index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
1254 !isWithinInterval(
1255 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod),
d632062f 1256 compositeInterval,
ef9e3b33
JB
1257 ) &&
1258 isWithinInterval(
1259 addSeconds(
1260 chargingScheduleInterval.start,
1261 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
1262 ),
d632062f 1263 compositeInterval,
ef9e3b33
JB
1264 )
1265 ) {
ef9e3b33
JB
1266 return true;
1267 }
1268 return false;
0e14e1d4
JB
1269 })
1270 .map((schedulePeriod, index) => {
1271 if (index === 0 && schedulePeriod.startPeriod !== 0) {
1272 schedulePeriod.startPeriod = 0;
1273 }
1274 return schedulePeriod;
1275 }),
ef9e3b33
JB
1276 };
1277 }
d632062f 1278 if (isAfter(chargingScheduleInterval.end, compositeInterval.end)) {
ef9e3b33
JB
1279 return {
1280 ...chargingSchedule,
d632062f
JB
1281 duration: differenceInSeconds(
1282 compositeInterval.end as Date,
1283 chargingScheduleInterval.start,
1284 ),
ef9e3b33
JB
1285 chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter((schedulePeriod) =>
1286 isWithinInterval(
1287 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
d632062f 1288 compositeInterval,
ef9e3b33
JB
1289 ),
1290 ),
1291 };
1292 }
1293 return chargingSchedule;
1294 }
1295 };
1296
7bc31f9c
JB
1297 private static buildSampledValue(
1298 sampledValueTemplate: SampledValueTemplate,
1299 value: number,
1300 context?: MeterValueContext,
5edd8ba0 1301 phase?: OCPP16MeterValuePhase,
7bc31f9c 1302 ): OCPP16SampledValue {
4ed03b6e
JB
1303 const sampledValueValue = value ?? sampledValueTemplate?.value;
1304 const sampledValueContext = context ?? sampledValueTemplate?.context;
7bc31f9c
JB
1305 const sampledValueLocation =
1306 sampledValueTemplate?.location ??
e1d9a0f4 1307 OCPP16ServiceUtils.getMeasurandDefaultLocation(sampledValueTemplate.measurand!);
4ed03b6e 1308 const sampledValuePhase = phase ?? sampledValueTemplate?.phase;
7bc31f9c 1309 return {
9bf0ef23 1310 ...(!isNullOrUndefined(sampledValueTemplate.unit) && {
7bc31f9c
JB
1311 unit: sampledValueTemplate.unit,
1312 }),
9bf0ef23
JB
1313 ...(!isNullOrUndefined(sampledValueContext) && { context: sampledValueContext }),
1314 ...(!isNullOrUndefined(sampledValueTemplate.measurand) && {
7bc31f9c
JB
1315 measurand: sampledValueTemplate.measurand,
1316 }),
9bf0ef23
JB
1317 ...(!isNullOrUndefined(sampledValueLocation) && { location: sampledValueLocation }),
1318 ...(!isNullOrUndefined(sampledValueValue) && { value: sampledValueValue.toString() }),
1319 ...(!isNullOrUndefined(sampledValuePhase) && { phase: sampledValuePhase }),
e1d9a0f4 1320 } as OCPP16SampledValue;
7bc31f9c
JB
1321 }
1322
1323 private static checkMeasurandPowerDivider(
1324 chargingStation: ChargingStation,
5edd8ba0 1325 measurandType: OCPP16MeterValueMeasurand,
7bc31f9c 1326 ): void {
9bf0ef23 1327 if (isUndefined(chargingStation.powerDivider)) {
fc040c43 1328 const errMsg = `MeterValues measurand ${
7bc31f9c
JB
1329 measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1330 }: powerDivider is undefined`;
fc040c43 1331 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
7bc31f9c 1332 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
fa7bccf4 1333 } else if (chargingStation?.powerDivider <= 0) {
fc040c43 1334 const errMsg = `MeterValues measurand ${
7bc31f9c 1335 measurandType ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
fa7bccf4 1336 }: powerDivider have zero or below value ${chargingStation.powerDivider}`;
fc040c43 1337 logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
7bc31f9c
JB
1338 throw new OCPPError(ErrorType.INTERNAL_ERROR, errMsg, OCPP16RequestCommand.METER_VALUES);
1339 }
1340 }
1341
1342 private static getMeasurandDefaultLocation(
5edd8ba0 1343 measurandType: OCPP16MeterValueMeasurand,
7bc31f9c
JB
1344 ): MeterValueLocation | undefined {
1345 switch (measurandType) {
1346 case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
1347 return MeterValueLocation.EV;
1348 }
1349 }
1350
3b0ed034
JB
1351 // private static getMeasurandDefaultUnit(
1352 // measurandType: OCPP16MeterValueMeasurand,
1353 // ): MeterValueUnit | undefined {
1354 // switch (measurandType) {
1355 // case OCPP16MeterValueMeasurand.CURRENT_EXPORT:
1356 // case OCPP16MeterValueMeasurand.CURRENT_IMPORT:
1357 // case OCPP16MeterValueMeasurand.CURRENT_OFFERED:
1358 // return MeterValueUnit.AMP;
1359 // case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_EXPORT_REGISTER:
1360 // case OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER:
1361 // return MeterValueUnit.WATT_HOUR;
1362 // case OCPP16MeterValueMeasurand.POWER_ACTIVE_EXPORT:
1363 // case OCPP16MeterValueMeasurand.POWER_ACTIVE_IMPORT:
1364 // case OCPP16MeterValueMeasurand.POWER_OFFERED:
1365 // return MeterValueUnit.WATT;
1366 // case OCPP16MeterValueMeasurand.STATE_OF_CHARGE:
1367 // return MeterValueUnit.PERCENT;
1368 // case OCPP16MeterValueMeasurand.VOLTAGE:
1369 // return MeterValueUnit.VOLT;
1370 // }
1371 // }
6ed92bc1 1372}