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