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