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