fix: ensure the latest schedule period within the charging profile
[e-mobility-charging-stations-simulator.git] / src / charging-station / Helpers.ts
CommitLineData
d972af76
JB
1import { createHash, randomBytes } from 'node:crypto';
2import type { EventEmitter } from 'node:events';
3import { basename, dirname, join } from 'node:path';
130783a7 4import { fileURLToPath } from 'node:url';
8114d10e 5
e302df1d 6import chalk from 'chalk';
f924d466
JB
7import {
8 addDays,
f924d466
JB
9 addSeconds,
10 addWeeks,
497588ef 11 differenceInDays,
d476bc1b 12 differenceInSeconds,
497588ef 13 differenceInWeeks,
f924d466
JB
14 isAfter,
15 isBefore,
0bd926c1 16 isDate,
90aceaf6 17 isPast,
252a7d22 18 isWithinInterval,
522e4b05 19 toDate,
f924d466 20} from 'date-fns';
8114d10e 21
4c3c0d59 22import type { ChargingStation } from './ChargingStation';
357a5553 23import { getConfigurationKey } from './ConfigurationKeyUtils';
268a74bb 24import { BaseError } from '../exception';
981ebfbe 25import {
492cf6ab 26 AmpereUnits,
04b1261c 27 AvailabilityType,
268a74bb
JB
28 type BootNotificationRequest,
29 BootReasonEnumType,
15068be9 30 type ChargingProfile,
268a74bb 31 ChargingProfileKindType,
15068be9
JB
32 ChargingRateUnitType,
33 type ChargingSchedulePeriod,
268a74bb
JB
34 type ChargingStationInfo,
35 type ChargingStationTemplate,
b1f1b0f6 36 ChargingStationWorkerMessageEvents,
dd08d43d 37 ConnectorPhaseRotation,
a78ef5ed 38 type ConnectorStatus,
c3b83130 39 ConnectorStatusEnum,
268a74bb 40 CurrentType,
ae25f265 41 type EvseTemplate,
268a74bb
JB
42 type OCPP16BootNotificationRequest,
43 type OCPP20BootNotificationRequest,
44 OCPPVersion,
45 RecurrencyKindType,
90aceaf6
JB
46 type Reservation,
47 ReservationTerminationReason,
d8093be1
JB
48 StandardParametersKey,
49 SupportedFeatureProfiles,
268a74bb
JB
50 Voltage,
51} from '../types';
9bf0ef23
JB
52import {
53 ACElectricUtils,
54 Constants,
55 DCElectricUtils,
56 cloneObject,
b85cef4c 57 convertToDate,
9bf0ef23 58 convertToInt,
80c58041 59 isArraySorted,
9bf0ef23
JB
60 isEmptyObject,
61 isEmptyString,
62 isNotEmptyArray,
63 isNotEmptyString,
64 isNullOrUndefined,
65 isUndefined,
0bd926c1 66 isValidTime,
9bf0ef23
JB
67 logger,
68 secureRandom,
69} from '../utils';
17ac262c 70
08b58f00 71const moduleName = 'Helpers';
91a4f151 72
fba11dc6
JB
73export const getChargingStationId = (
74 index: number,
5edd8ba0 75 stationTemplate: ChargingStationTemplate,
fba11dc6
JB
76): string => {
77 // In case of multiple instances: add instance index to charging station id
78 const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
79 const idSuffix = stationTemplate?.nameSuffix ?? '';
80 const idStr = `000000000${index.toString()}`;
81 return stationTemplate?.fixedName
82 ? stationTemplate.baseName
83 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
5edd8ba0 84 idStr.length - 4,
fba11dc6
JB
85 )}${idSuffix}`;
86};
87
90aceaf6
JB
88export const hasReservationExpired = (reservation: Reservation): boolean => {
89 return isPast(reservation.expiryDate);
90};
91
92export const removeExpiredReservations = async (
93 chargingStation: ChargingStation,
94): Promise<void> => {
95 if (chargingStation.hasEvses) {
96 for (const evseStatus of chargingStation.evses.values()) {
97 for (const connectorStatus of evseStatus.connectors.values()) {
98 if (connectorStatus.reservation && hasReservationExpired(connectorStatus.reservation)) {
99 await chargingStation.removeReservation(
100 connectorStatus.reservation,
101 ReservationTerminationReason.EXPIRED,
102 );
103 }
104 }
105 }
106 } else {
107 for (const connectorStatus of chargingStation.connectors.values()) {
108 if (connectorStatus.reservation && hasReservationExpired(connectorStatus.reservation)) {
109 await chargingStation.removeReservation(
110 connectorStatus.reservation,
111 ReservationTerminationReason.EXPIRED,
112 );
113 }
114 }
115 }
116};
117
118export const getNumberOfReservableConnectors = (
119 connectors: Map<number, ConnectorStatus>,
120): number => {
cfc9875a 121 let numberOfReservableConnectors = 0;
fba11dc6
JB
122 for (const [connectorId, connectorStatus] of connectors) {
123 if (connectorId === 0) {
124 continue;
3fa7f799 125 }
fba11dc6 126 if (connectorStatus.status === ConnectorStatusEnum.Available) {
cfc9875a 127 ++numberOfReservableConnectors;
1bf29f5b 128 }
1bf29f5b 129 }
cfc9875a 130 return numberOfReservableConnectors;
fba11dc6
JB
131};
132
133export const getHashId = (index: number, stationTemplate: ChargingStationTemplate): string => {
134 const chargingStationInfo = {
135 chargePointModel: stationTemplate.chargePointModel,
136 chargePointVendor: stationTemplate.chargePointVendor,
137 ...(!isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && {
138 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix,
139 }),
140 ...(!isUndefined(stationTemplate.chargePointSerialNumberPrefix) && {
141 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix,
142 }),
143 ...(!isUndefined(stationTemplate.meterSerialNumberPrefix) && {
144 meterSerialNumber: stationTemplate.meterSerialNumberPrefix,
145 }),
146 ...(!isUndefined(stationTemplate.meterType) && {
147 meterType: stationTemplate.meterType,
148 }),
149 };
150 return createHash(Constants.DEFAULT_HASH_ALGORITHM)
151 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
152 .digest('hex');
153};
154
155export const checkChargingStation = (
156 chargingStation: ChargingStation,
5edd8ba0 157 logPrefix: string,
fba11dc6
JB
158): boolean => {
159 if (chargingStation.started === false && chargingStation.starting === false) {
160 logger.warn(`${logPrefix} charging station is stopped, cannot proceed`);
161 return false;
162 }
163 return true;
164};
165
166export const getPhaseRotationValue = (
167 connectorId: number,
5edd8ba0 168 numberOfPhases: number,
fba11dc6
JB
169): string | undefined => {
170 // AC/DC
171 if (connectorId === 0 && numberOfPhases === 0) {
172 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
173 } else if (connectorId > 0 && numberOfPhases === 0) {
174 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
175 // AC
176 } else if (connectorId > 0 && numberOfPhases === 1) {
177 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
178 } else if (connectorId > 0 && numberOfPhases === 3) {
179 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
180 }
181};
182
183export const getMaxNumberOfEvses = (evses: Record<string, EvseTemplate>): number => {
184 if (!evses) {
185 return -1;
186 }
187 return Object.keys(evses).length;
188};
189
190const getMaxNumberOfConnectors = (connectors: Record<string, ConnectorStatus>): number => {
191 if (!connectors) {
192 return -1;
193 }
194 return Object.keys(connectors).length;
195};
196
197export const getBootConnectorStatus = (
198 chargingStation: ChargingStation,
199 connectorId: number,
5edd8ba0 200 connectorStatus: ConnectorStatus,
fba11dc6
JB
201): ConnectorStatusEnum => {
202 let connectorBootStatus: ConnectorStatusEnum;
203 if (
204 !connectorStatus?.status &&
205 (chargingStation.isChargingStationAvailable() === false ||
206 chargingStation.isConnectorAvailable(connectorId) === false)
207 ) {
208 connectorBootStatus = ConnectorStatusEnum.Unavailable;
209 } else if (!connectorStatus?.status && connectorStatus?.bootStatus) {
210 // Set boot status in template at startup
211 connectorBootStatus = connectorStatus?.bootStatus;
212 } else if (connectorStatus?.status) {
213 // Set previous status at startup
214 connectorBootStatus = connectorStatus?.status;
215 } else {
216 // Set default status
217 connectorBootStatus = ConnectorStatusEnum.Available;
218 }
219 return connectorBootStatus;
220};
221
222export const checkTemplate = (
223 stationTemplate: ChargingStationTemplate,
224 logPrefix: string,
5edd8ba0 225 templateFile: string,
fba11dc6
JB
226): void => {
227 if (isNullOrUndefined(stationTemplate)) {
228 const errorMsg = `Failed to read charging station template file ${templateFile}`;
229 logger.error(`${logPrefix} ${errorMsg}`);
230 throw new BaseError(errorMsg);
231 }
232 if (isEmptyObject(stationTemplate)) {
233 const errorMsg = `Empty charging station information from template file ${templateFile}`;
234 logger.error(`${logPrefix} ${errorMsg}`);
235 throw new BaseError(errorMsg);
236 }
e1d9a0f4 237 if (isEmptyObject(stationTemplate.AutomaticTransactionGenerator!)) {
fba11dc6
JB
238 stationTemplate.AutomaticTransactionGenerator = Constants.DEFAULT_ATG_CONFIGURATION;
239 logger.warn(
240 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
5edd8ba0 241 Constants.DEFAULT_ATG_CONFIGURATION,
fba11dc6 242 );
fa7bccf4 243 }
fba11dc6
JB
244 if (isNullOrUndefined(stationTemplate.idTagsFile) || isEmptyString(stationTemplate.idTagsFile)) {
245 logger.warn(
5edd8ba0 246 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`,
fba11dc6 247 );
c3b83130 248 }
fba11dc6
JB
249};
250
251export const checkConnectorsConfiguration = (
252 stationTemplate: ChargingStationTemplate,
253 logPrefix: string,
5edd8ba0 254 templateFile: string,
fba11dc6
JB
255): {
256 configuredMaxConnectors: number;
257 templateMaxConnectors: number;
258 templateMaxAvailableConnectors: number;
259} => {
cfc9875a 260 const configuredMaxConnectors = getConfiguredMaxNumberOfConnectors(stationTemplate);
fba11dc6 261 checkConfiguredMaxConnectors(configuredMaxConnectors, logPrefix, templateFile);
e1d9a0f4 262 const templateMaxConnectors = getMaxNumberOfConnectors(stationTemplate.Connectors!);
fba11dc6 263 checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile);
e1d9a0f4 264 const templateMaxAvailableConnectors = stationTemplate.Connectors![0]
fba11dc6
JB
265 ? templateMaxConnectors - 1
266 : templateMaxConnectors;
267 if (
268 configuredMaxConnectors > templateMaxAvailableConnectors &&
269 !stationTemplate?.randomConnectors
8a133cc8 270 ) {
fba11dc6 271 logger.warn(
5edd8ba0 272 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`,
cda5d0fb 273 );
fba11dc6
JB
274 stationTemplate.randomConnectors = true;
275 }
276 return { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors };
277};
278
279export const checkStationInfoConnectorStatus = (
280 connectorId: number,
281 connectorStatus: ConnectorStatus,
282 logPrefix: string,
5edd8ba0 283 templateFile: string,
fba11dc6
JB
284): void => {
285 if (!isNullOrUndefined(connectorStatus?.status)) {
286 logger.warn(
5edd8ba0 287 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`,
cda5d0fb 288 );
fba11dc6
JB
289 delete connectorStatus.status;
290 }
291};
292
293export const buildConnectorsMap = (
294 connectors: Record<string, ConnectorStatus>,
295 logPrefix: string,
5edd8ba0 296 templateFile: string,
fba11dc6
JB
297): Map<number, ConnectorStatus> => {
298 const connectorsMap = new Map<number, ConnectorStatus>();
299 if (getMaxNumberOfConnectors(connectors) > 0) {
300 for (const connector in connectors) {
301 const connectorStatus = connectors[connector];
302 const connectorId = convertToInt(connector);
303 checkStationInfoConnectorStatus(connectorId, connectorStatus, logPrefix, templateFile);
304 connectorsMap.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus));
fa7bccf4 305 }
fba11dc6
JB
306 } else {
307 logger.warn(
5edd8ba0 308 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`,
fba11dc6 309 );
fa7bccf4 310 }
fba11dc6
JB
311 return connectorsMap;
312};
fa7bccf4 313
fba11dc6
JB
314export const initializeConnectorsMapStatus = (
315 connectors: Map<number, ConnectorStatus>,
5edd8ba0 316 logPrefix: string,
fba11dc6
JB
317): void => {
318 for (const connectorId of connectors.keys()) {
319 if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
04b1261c 320 logger.warn(
5edd8ba0
JB
321 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
322 connectorId,
323 )?.transactionId}`,
04b1261c 324 );
04b1261c 325 }
fba11dc6 326 if (connectorId === 0) {
e1d9a0f4 327 connectors.get(connectorId)!.availability = AvailabilityType.Operative;
fba11dc6 328 if (isUndefined(connectors.get(connectorId)?.chargingProfiles)) {
e1d9a0f4 329 connectors.get(connectorId)!.chargingProfiles = [];
04b1261c 330 }
fba11dc6
JB
331 } else if (
332 connectorId > 0 &&
333 isNullOrUndefined(connectors.get(connectorId)?.transactionStarted)
334 ) {
e1d9a0f4 335 initializeConnectorStatus(connectors.get(connectorId)!);
04b1261c
JB
336 }
337 }
fba11dc6
JB
338};
339
340export const resetConnectorStatus = (connectorStatus: ConnectorStatus): void => {
341 connectorStatus.idTagLocalAuthorized = false;
342 connectorStatus.idTagAuthorized = false;
343 connectorStatus.transactionRemoteStarted = false;
344 connectorStatus.transactionStarted = false;
a71d4e70
JB
345 delete connectorStatus?.transactionStart;
346 delete connectorStatus?.transactionId;
fba11dc6
JB
347 delete connectorStatus?.localAuthorizeIdTag;
348 delete connectorStatus?.authorizeIdTag;
fba11dc6
JB
349 delete connectorStatus?.transactionIdTag;
350 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
351 delete connectorStatus?.transactionBeginMeterValue;
352};
353
354export const createBootNotificationRequest = (
355 stationInfo: ChargingStationInfo,
5edd8ba0 356 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp,
fba11dc6
JB
357): BootNotificationRequest => {
358 const ocppVersion = stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
359 switch (ocppVersion) {
360 case OCPPVersion.VERSION_16:
361 return {
362 chargePointModel: stationInfo.chargePointModel,
363 chargePointVendor: stationInfo.chargePointVendor,
364 ...(!isUndefined(stationInfo.chargeBoxSerialNumber) && {
365 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber,
366 }),
367 ...(!isUndefined(stationInfo.chargePointSerialNumber) && {
368 chargePointSerialNumber: stationInfo.chargePointSerialNumber,
369 }),
370 ...(!isUndefined(stationInfo.firmwareVersion) && {
371 firmwareVersion: stationInfo.firmwareVersion,
372 }),
373 ...(!isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
374 ...(!isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
375 ...(!isUndefined(stationInfo.meterSerialNumber) && {
376 meterSerialNumber: stationInfo.meterSerialNumber,
377 }),
378 ...(!isUndefined(stationInfo.meterType) && {
379 meterType: stationInfo.meterType,
380 }),
381 } as OCPP16BootNotificationRequest;
382 case OCPPVersion.VERSION_20:
383 case OCPPVersion.VERSION_201:
384 return {
385 reason: bootReason,
386 chargingStation: {
387 model: stationInfo.chargePointModel,
388 vendorName: stationInfo.chargePointVendor,
9bf0ef23 389 ...(!isUndefined(stationInfo.firmwareVersion) && {
d270cc87
JB
390 firmwareVersion: stationInfo.firmwareVersion,
391 }),
fba11dc6
JB
392 ...(!isUndefined(stationInfo.chargeBoxSerialNumber) && {
393 serialNumber: stationInfo.chargeBoxSerialNumber,
d270cc87 394 }),
fba11dc6
JB
395 ...((!isUndefined(stationInfo.iccid) || !isUndefined(stationInfo.imsi)) && {
396 modem: {
397 ...(!isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
398 ...(!isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
399 },
d270cc87 400 }),
fba11dc6
JB
401 },
402 } as OCPP20BootNotificationRequest;
403 }
404};
405
406export const warnTemplateKeysDeprecation = (
407 stationTemplate: ChargingStationTemplate,
408 logPrefix: string,
5edd8ba0 409 templateFile: string,
fba11dc6 410) => {
e4c6cf05
JB
411 const templateKeys: { deprecatedKey: string; key?: string }[] = [
412 { deprecatedKey: 'supervisionUrl', key: 'supervisionUrls' },
413 { deprecatedKey: 'authorizationFile', key: 'idTagsFile' },
414 { deprecatedKey: 'payloadSchemaValidation', key: 'ocppStrictCompliance' },
cfdf901d 415 { deprecatedKey: 'mustAuthorizeAtRemoteStart', key: 'remoteAuthorization' },
fba11dc6
JB
416 ];
417 for (const templateKey of templateKeys) {
418 warnDeprecatedTemplateKey(
419 stationTemplate,
420 templateKey.deprecatedKey,
421 logPrefix,
422 templateFile,
e1d9a0f4 423 !isUndefined(templateKey.key) ? `Use '${templateKey.key}' instead` : undefined,
fba11dc6
JB
424 );
425 convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key);
426 }
427};
428
429export const stationTemplateToStationInfo = (
5edd8ba0 430 stationTemplate: ChargingStationTemplate,
fba11dc6
JB
431): ChargingStationInfo => {
432 stationTemplate = cloneObject<ChargingStationTemplate>(stationTemplate);
433 delete stationTemplate.power;
434 delete stationTemplate.powerUnit;
e1d9a0f4
JB
435 delete stationTemplate.Connectors;
436 delete stationTemplate.Evses;
fba11dc6
JB
437 delete stationTemplate.Configuration;
438 delete stationTemplate.AutomaticTransactionGenerator;
439 delete stationTemplate.chargeBoxSerialNumberPrefix;
440 delete stationTemplate.chargePointSerialNumberPrefix;
441 delete stationTemplate.meterSerialNumberPrefix;
66b537dc 442 return stationTemplate as ChargingStationInfo;
fba11dc6
JB
443};
444
445export const createSerialNumber = (
446 stationTemplate: ChargingStationTemplate,
447 stationInfo: ChargingStationInfo,
448 params: {
449 randomSerialNumberUpperCase?: boolean;
450 randomSerialNumber?: boolean;
451 } = {
452 randomSerialNumberUpperCase: true,
453 randomSerialNumber: true,
5edd8ba0 454 },
fba11dc6
JB
455): void => {
456 params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params };
457 const serialNumberSuffix = params?.randomSerialNumber
458 ? getRandomSerialNumberSuffix({
459 upperCase: params.randomSerialNumberUpperCase,
460 })
461 : '';
462 isNotEmptyString(stationTemplate?.chargePointSerialNumberPrefix) &&
463 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
464 isNotEmptyString(stationTemplate?.chargeBoxSerialNumberPrefix) &&
465 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
466 isNotEmptyString(stationTemplate?.meterSerialNumberPrefix) &&
467 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
468};
469
470export const propagateSerialNumber = (
471 stationTemplate: ChargingStationTemplate,
472 stationInfoSrc: ChargingStationInfo,
5edd8ba0 473 stationInfoDst: ChargingStationInfo,
fba11dc6
JB
474) => {
475 if (!stationInfoSrc || !stationTemplate) {
476 throw new BaseError(
5edd8ba0 477 'Missing charging station template or existing configuration to propagate serial number',
fba11dc6 478 );
17ac262c 479 }
fba11dc6
JB
480 stationTemplate?.chargePointSerialNumberPrefix && stationInfoSrc?.chargePointSerialNumber
481 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
482 : stationInfoDst?.chargePointSerialNumber && delete stationInfoDst.chargePointSerialNumber;
483 stationTemplate?.chargeBoxSerialNumberPrefix && stationInfoSrc?.chargeBoxSerialNumber
484 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
485 : stationInfoDst?.chargeBoxSerialNumber && delete stationInfoDst.chargeBoxSerialNumber;
486 stationTemplate?.meterSerialNumberPrefix && stationInfoSrc?.meterSerialNumber
487 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
488 : stationInfoDst?.meterSerialNumber && delete stationInfoDst.meterSerialNumber;
489};
490
d8093be1
JB
491export const hasFeatureProfile = (
492 chargingStation: ChargingStation,
493 featureProfile: SupportedFeatureProfiles,
494): boolean | undefined => {
495 return getConfigurationKey(
496 chargingStation,
497 StandardParametersKey.SupportedFeatureProfiles,
498 )?.value?.includes(featureProfile);
499};
500
fba11dc6
JB
501export const getAmperageLimitationUnitDivider = (stationInfo: ChargingStationInfo): number => {
502 let unitDivider = 1;
503 switch (stationInfo.amperageLimitationUnit) {
504 case AmpereUnits.DECI_AMPERE:
505 unitDivider = 10;
506 break;
507 case AmpereUnits.CENTI_AMPERE:
508 unitDivider = 100;
509 break;
510 case AmpereUnits.MILLI_AMPERE:
511 unitDivider = 1000;
512 break;
513 }
514 return unitDivider;
515};
516
517export const getChargingStationConnectorChargingProfilesPowerLimit = (
518 chargingStation: ChargingStation,
5edd8ba0 519 connectorId: number,
fba11dc6 520): number | undefined => {
bbb55ee4 521 let limit: number | undefined, chargingProfile: ChargingProfile | undefined;
252a7d22 522 // Get charging profiles for connector id and sort by stack level
ad490d5f
JB
523 const chargingProfiles = cloneObject<ChargingProfile[]>(
524 (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? []).concat(
525 chargingStation.getConnectorStatus(0)?.chargingProfiles ?? [],
526 ),
527 ).sort((a, b) => b.stackLevel - a.stackLevel);
fba11dc6 528 if (isNotEmptyArray(chargingProfiles)) {
a71d4e70
JB
529 const result = getLimitFromChargingProfiles(
530 chargingStation,
531 connectorId,
532 chargingProfiles,
533 chargingStation.logPrefix(),
534 );
fba11dc6
JB
535 if (!isNullOrUndefined(result)) {
536 limit = result?.limit;
bbb55ee4 537 chargingProfile = result?.chargingProfile;
fba11dc6
JB
538 switch (chargingStation.getCurrentOutType()) {
539 case CurrentType.AC:
540 limit =
bbb55ee4 541 chargingProfile?.chargingSchedule?.chargingRateUnit === ChargingRateUnitType.WATT
fba11dc6
JB
542 ? limit
543 : ACElectricUtils.powerTotal(
544 chargingStation.getNumberOfPhases(),
545 chargingStation.getVoltageOut(),
e1d9a0f4 546 limit!,
fba11dc6
JB
547 );
548 break;
549 case CurrentType.DC:
550 limit =
bbb55ee4 551 chargingProfile?.chargingSchedule?.chargingRateUnit === ChargingRateUnitType.WATT
fba11dc6 552 ? limit
e1d9a0f4 553 : DCElectricUtils.power(chargingStation.getVoltageOut(), limit!);
fba11dc6
JB
554 }
555 const connectorMaximumPower =
556 chargingStation.getMaximumPower() / chargingStation.powerDivider;
e1d9a0f4 557 if (limit! > connectorMaximumPower) {
fba11dc6 558 logger.error(
bbb55ee4 559 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${chargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
5edd8ba0 560 result,
fba11dc6
JB
561 );
562 limit = connectorMaximumPower;
15068be9
JB
563 }
564 }
15068be9 565 }
fba11dc6
JB
566 return limit;
567};
568
569export const getDefaultVoltageOut = (
570 currentType: CurrentType,
571 logPrefix: string,
5edd8ba0 572 templateFile: string,
fba11dc6
JB
573): Voltage => {
574 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
575 let defaultVoltageOut: number;
576 switch (currentType) {
577 case CurrentType.AC:
578 defaultVoltageOut = Voltage.VOLTAGE_230;
579 break;
580 case CurrentType.DC:
581 defaultVoltageOut = Voltage.VOLTAGE_400;
582 break;
583 default:
584 logger.error(`${logPrefix} ${errorMsg}`);
585 throw new BaseError(errorMsg);
15068be9 586 }
fba11dc6
JB
587 return defaultVoltageOut;
588};
589
590export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
591 return (
592 stationInfo.idTagsFile &&
593 join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
594 );
595};
596
b2b60626 597export const waitChargingStationEvents = async (
fba11dc6
JB
598 emitter: EventEmitter,
599 event: ChargingStationWorkerMessageEvents,
5edd8ba0 600 eventsToWait: number,
fba11dc6 601): Promise<number> => {
474d4ffc 602 return new Promise<number>((resolve) => {
fba11dc6
JB
603 let events = 0;
604 if (eventsToWait === 0) {
605 resolve(events);
606 }
607 emitter.on(event, () => {
608 ++events;
609 if (events === eventsToWait) {
b1f1b0f6
JB
610 resolve(events);
611 }
b1f1b0f6 612 });
fba11dc6
JB
613 });
614};
615
cfc9875a
JB
616const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
617 let configuredMaxNumberOfConnectors = 0;
fba11dc6
JB
618 if (isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
619 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
cfc9875a 620 configuredMaxNumberOfConnectors =
fba11dc6
JB
621 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)];
622 } else if (isUndefined(stationTemplate.numberOfConnectors) === false) {
cfc9875a 623 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors as number;
fba11dc6 624 } else if (stationTemplate.Connectors && !stationTemplate.Evses) {
cfc9875a 625 configuredMaxNumberOfConnectors = stationTemplate.Connectors[0]
fba11dc6
JB
626 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
627 : getMaxNumberOfConnectors(stationTemplate.Connectors);
628 } else if (stationTemplate.Evses && !stationTemplate.Connectors) {
fba11dc6
JB
629 for (const evse in stationTemplate.Evses) {
630 if (evse === '0') {
631 continue;
cda5d0fb 632 }
cfc9875a
JB
633 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
634 stationTemplate.Evses[evse].Connectors,
635 );
cda5d0fb 636 }
cda5d0fb 637 }
cfc9875a 638 return configuredMaxNumberOfConnectors;
fba11dc6
JB
639};
640
641const checkConfiguredMaxConnectors = (
642 configuredMaxConnectors: number,
643 logPrefix: string,
5edd8ba0 644 templateFile: string,
fba11dc6
JB
645): void => {
646 if (configuredMaxConnectors <= 0) {
647 logger.warn(
5edd8ba0 648 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
fba11dc6 649 );
cda5d0fb 650 }
fba11dc6 651};
cda5d0fb 652
fba11dc6
JB
653const checkTemplateMaxConnectors = (
654 templateMaxConnectors: number,
655 logPrefix: string,
5edd8ba0 656 templateFile: string,
fba11dc6
JB
657): void => {
658 if (templateMaxConnectors === 0) {
659 logger.warn(
5edd8ba0 660 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
fba11dc6
JB
661 );
662 } else if (templateMaxConnectors < 0) {
663 logger.error(
5edd8ba0 664 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
fba11dc6
JB
665 );
666 }
667};
668
669const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
670 connectorStatus.availability = AvailabilityType.Operative;
671 connectorStatus.idTagLocalAuthorized = false;
672 connectorStatus.idTagAuthorized = false;
673 connectorStatus.transactionRemoteStarted = false;
674 connectorStatus.transactionStarted = false;
675 connectorStatus.energyActiveImportRegisterValue = 0;
676 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
677 if (isUndefined(connectorStatus.chargingProfiles)) {
678 connectorStatus.chargingProfiles = [];
679 }
680};
681
682const warnDeprecatedTemplateKey = (
683 template: ChargingStationTemplate,
684 key: string,
685 logPrefix: string,
686 templateFile: string,
5edd8ba0 687 logMsgToAppend = '',
fba11dc6 688): void => {
a37fc6dc 689 if (!isUndefined(template[key as keyof ChargingStationTemplate])) {
fba11dc6
JB
690 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
691 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
692 }`;
693 logger.warn(`${logPrefix} ${logMsg}`);
694 console.warn(chalk.yellow(`${logMsg}`));
695 }
696};
697
698const convertDeprecatedTemplateKey = (
699 template: ChargingStationTemplate,
700 deprecatedKey: string,
e1d9a0f4 701 key?: string,
fba11dc6 702): void => {
a37fc6dc 703 if (!isUndefined(template[deprecatedKey as keyof ChargingStationTemplate])) {
e1d9a0f4 704 if (!isUndefined(key)) {
a37fc6dc
JB
705 (template as unknown as Record<string, unknown>)[key!] =
706 template[deprecatedKey as keyof ChargingStationTemplate];
e1d9a0f4 707 }
a37fc6dc 708 delete template[deprecatedKey as keyof ChargingStationTemplate];
fba11dc6
JB
709 }
710};
711
947f048a
JB
712interface ChargingProfilesLimit {
713 limit: number;
bbb55ee4 714 chargingProfile: ChargingProfile;
947f048a
JB
715}
716
fba11dc6 717/**
d467756c 718 * Charging profiles shall already be sorted by connector id and stack level (highest stack level has priority)
fba11dc6 719 *
d467756c
JB
720 * @param chargingStation -
721 * @param connectorId -
fba11dc6
JB
722 * @param chargingProfiles -
723 * @param logPrefix -
947f048a 724 * @returns ChargingProfilesLimit
fba11dc6
JB
725 */
726const getLimitFromChargingProfiles = (
a71d4e70
JB
727 chargingStation: ChargingStation,
728 connectorId: number,
fba11dc6 729 chargingProfiles: ChargingProfile[],
5edd8ba0 730 logPrefix: string,
947f048a 731): ChargingProfilesLimit | undefined => {
fba11dc6 732 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
fba11dc6 733 const currentDate = new Date();
0eb666db 734 const connectorStatus = chargingStation.getConnectorStatus(connectorId)!;
ad490d5f
JB
735 if (!isArraySorted(chargingProfiles, (a, b) => b.stackLevel - a.stackLevel)) {
736 logger.warn(
737 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profiles are not sorted by stack level. Trying to sort them`,
738 );
739 chargingProfiles.sort((a, b) => b.stackLevel - a.stackLevel);
740 }
fba11dc6 741 for (const chargingProfile of chargingProfiles) {
fba11dc6 742 const chargingSchedule = chargingProfile.chargingSchedule;
5543b88d 743 if (connectorStatus?.transactionStarted && isNullOrUndefined(chargingSchedule?.startSchedule)) {
109c677a 744 logger.debug(
ec4a242a 745 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`,
cda5d0fb 746 );
a71d4e70 747 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
b5c19509 748 chargingSchedule.startSchedule = connectorStatus?.transactionStart;
52952bf8 749 }
0bd926c1 750 if (!isDate(chargingSchedule?.startSchedule)) {
8d75a403 751 logger.warn(
ec4a242a 752 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date object. Trying to convert it to a Date object`,
8d75a403 753 );
991fb26b 754 chargingSchedule.startSchedule = convertToDate(chargingSchedule?.startSchedule)!;
8d75a403 755 }
0eb666db
JB
756 if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
757 continue;
ec4a242a 758 }
0bd926c1 759 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
142a66c9
JB
760 continue;
761 }
fba11dc6
JB
762 // Check if the charging profile is active
763 if (
0bd926c1 764 isValidTime(chargingSchedule?.startSchedule) &&
975e18ec
JB
765 isWithinInterval(currentDate, {
766 start: chargingSchedule.startSchedule!,
767 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
768 })
fba11dc6 769 ) {
252a7d22 770 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
80c58041
JB
771 const chargingSchedulePeriodCompareFn = (
772 a: ChargingSchedulePeriod,
773 b: ChargingSchedulePeriod,
774 ) => a.startPeriod - b.startPeriod;
775 if (
776 isArraySorted<ChargingSchedulePeriod>(
777 chargingSchedule.chargingSchedulePeriod,
778 chargingSchedulePeriodCompareFn,
779 ) === false
780 ) {
781 logger.warn(
782 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
783 );
784 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn);
785 }
975e18ec 786 // Check if the first schedule period start period is equal to 0
55f2ab60
JB
787 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
788 logger.error(
789 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
790 );
975e18ec 791 continue;
55f2ab60 792 }
991fb26b 793 // Handle only one schedule period
975e18ec 794 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
252a7d22
JB
795 const result: ChargingProfilesLimit = {
796 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
bbb55ee4 797 chargingProfile,
fba11dc6
JB
798 };
799 logger.debug(debugLogMsg, result);
800 return result;
41189456 801 }
e3037969 802 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined;
252a7d22 803 // Search for the right schedule period
e3037969
JB
804 for (const [
805 index,
806 chargingSchedulePeriod,
807 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
252a7d22
JB
808 // Find the right schedule period
809 if (
810 isAfter(
e3037969 811 addSeconds(chargingSchedule.startSchedule!, chargingSchedulePeriod.startPeriod),
252a7d22
JB
812 currentDate,
813 )
814 ) {
e3037969 815 // Found the schedule period: previous is the correct one
252a7d22 816 const result: ChargingProfilesLimit = {
e3037969 817 limit: previousChargingSchedulePeriod!.limit,
bbb55ee4 818 chargingProfile: chargingProfile,
252a7d22
JB
819 };
820 logger.debug(debugLogMsg, result);
821 return result;
822 }
e3037969
JB
823 // Keep a reference to previous one
824 previousChargingSchedulePeriod = chargingSchedulePeriod;
975e18ec 825 // Handle the last schedule period within the charging profile duration
252a7d22 826 if (
975e18ec
JB
827 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
828 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
ccfa30bc
JB
829 differenceInSeconds(
830 addSeconds(
d9dc6292 831 chargingSchedule.startSchedule!,
ccfa30bc
JB
832 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
833 ),
834 chargingSchedule.startSchedule!,
835 ) > chargingSchedule.duration!)
252a7d22
JB
836 ) {
837 const result: ChargingProfilesLimit = {
e3037969 838 limit: previousChargingSchedulePeriod.limit,
bbb55ee4 839 chargingProfile: chargingProfile,
252a7d22
JB
840 };
841 logger.debug(debugLogMsg, result);
842 return result;
843 }
17ac262c
JB
844 }
845 }
846 }
17ac262c 847 }
fba11dc6 848};
17ac262c 849
0eb666db
JB
850export const prepareChargingProfileKind = (
851 connectorStatus: ConnectorStatus,
852 chargingProfile: ChargingProfile,
853 currentDate: Date,
854 logPrefix: string,
855): boolean => {
856 switch (chargingProfile.chargingProfileKind) {
857 case ChargingProfileKindType.RECURRING:
858 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
859 return false;
860 }
861 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix);
862 break;
863 case ChargingProfileKindType.RELATIVE:
ccfa30bc
JB
864 if (!isNullOrUndefined(chargingProfile.chargingSchedule.startSchedule)) {
865 logger.warn(
866 `${logPrefix} ${moduleName}.prepareChargingProfileKind: Charging profile id ${chargingProfile.chargingProfileId} has a startSchedule property defined. It will be ignored or set to the connector current transaction start date`,
867 );
868 delete chargingProfile.chargingSchedule.startSchedule;
869 }
0eb666db
JB
870 connectorStatus?.transactionStarted &&
871 (chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart);
872 break;
873 }
874 return true;
875};
876
ad490d5f 877export const canProceedChargingProfile = (
0bd926c1
JB
878 chargingProfile: ChargingProfile,
879 currentDate: Date,
880 logPrefix: string,
881): boolean => {
882 if (
883 (isValidTime(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom!)) ||
884 (isValidTime(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo!))
885 ) {
886 logger.debug(
887 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
888 chargingProfile.chargingProfileId
889 } is not valid for the current date ${currentDate.toISOString()}`,
890 );
891 return false;
892 }
893 const chargingSchedule = chargingProfile.chargingSchedule;
894 if (isNullOrUndefined(chargingSchedule?.startSchedule)) {
895 logger.error(
ad490d5f 896 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`,
0bd926c1
JB
897 );
898 return false;
899 }
900 if (isNullOrUndefined(chargingSchedule?.duration)) {
901 logger.error(
902 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined, not yet supported`,
903 );
904 return false;
905 }
906 return true;
907};
908
0eb666db 909const canProceedRecurringChargingProfile = (
0bd926c1
JB
910 chargingProfile: ChargingProfile,
911 logPrefix: string,
912): boolean => {
913 if (
914 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
915 isNullOrUndefined(chargingProfile.recurrencyKind)
916 ) {
917 logger.error(
918 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
919 );
920 return false;
921 }
922 return true;
923};
924
522e4b05 925/**
ec4a242a 926 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
522e4b05
JB
927 *
928 * @param chargingProfile -
929 * @param currentDate -
930 * @param logPrefix -
931 */
0eb666db 932const prepareRecurringChargingProfile = (
76dab5a9
JB
933 chargingProfile: ChargingProfile,
934 currentDate: Date,
935 logPrefix: string,
ec4a242a 936): boolean => {
76dab5a9 937 const chargingSchedule = chargingProfile.chargingSchedule;
ec4a242a 938 let recurringIntervalTranslated = false;
522e4b05 939 let recurringInterval: Interval;
76dab5a9
JB
940 switch (chargingProfile.recurrencyKind) {
941 case RecurrencyKindType.DAILY:
522e4b05
JB
942 recurringInterval = {
943 start: chargingSchedule.startSchedule!,
944 end: addDays(chargingSchedule.startSchedule!, 1),
945 };
d476bc1b 946 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
522e4b05
JB
947 if (
948 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 949 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
950 ) {
951 chargingSchedule.startSchedule = addDays(
991fb26b 952 recurringInterval.start,
05b52716 953 differenceInDays(currentDate, recurringInterval.start),
76dab5a9 954 );
522e4b05
JB
955 recurringInterval = {
956 start: chargingSchedule.startSchedule,
957 end: addDays(chargingSchedule.startSchedule, 1),
958 };
ec4a242a 959 recurringIntervalTranslated = true;
76dab5a9
JB
960 }
961 break;
962 case RecurrencyKindType.WEEKLY:
522e4b05
JB
963 recurringInterval = {
964 start: chargingSchedule.startSchedule!,
965 end: addWeeks(chargingSchedule.startSchedule!, 1),
966 };
d476bc1b 967 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
522e4b05
JB
968 if (
969 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 970 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
971 ) {
972 chargingSchedule.startSchedule = addWeeks(
991fb26b 973 recurringInterval.start,
05b52716 974 differenceInWeeks(currentDate, recurringInterval.start),
76dab5a9 975 );
522e4b05
JB
976 recurringInterval = {
977 start: chargingSchedule.startSchedule,
978 end: addWeeks(chargingSchedule.startSchedule, 1),
979 };
ec4a242a 980 recurringIntervalTranslated = true;
76dab5a9
JB
981 }
982 break;
ec4a242a
JB
983 default:
984 logger.error(
985 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} recurrency kind ${chargingProfile.recurrencyKind} is not supported`,
986 );
76dab5a9 987 }
ec4a242a 988 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
522e4b05 989 logger.error(
aa5c5ad4 990 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
522e4b05 991 chargingProfile.recurrencyKind
991fb26b 992 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
522e4b05 993 recurringInterval!.start,
991fb26b
JB
994 ).toISOString()}, ${toDate(
995 recurringInterval!.end,
ec4a242a 996 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
522e4b05
JB
997 );
998 }
ec4a242a 999 return recurringIntervalTranslated;
76dab5a9
JB
1000};
1001
d476bc1b
JB
1002const checkRecurringChargingProfileDuration = (
1003 chargingProfile: ChargingProfile,
1004 interval: Interval,
1005 logPrefix: string,
ec4a242a 1006): void => {
142a66c9
JB
1007 if (isNullOrUndefined(chargingProfile.chargingSchedule.duration)) {
1008 logger.warn(
1009 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1010 chargingProfile.chargingProfileKind
1011 } charging profile id ${
1012 chargingProfile.chargingProfileId
1013 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1014 interval.end,
1015 interval.start,
1016 )}`,
1017 );
1018 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
1019 } else if (
d476bc1b
JB
1020 chargingProfile.chargingSchedule.duration! > differenceInSeconds(interval.end, interval.start)
1021 ) {
1022 logger.warn(
aa5c5ad4 1023 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
d476bc1b
JB
1024 chargingProfile.chargingProfileKind
1025 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1026 chargingProfile.chargingSchedule.duration
710d50eb 1027 } is greater than the recurrency time interval duration ${differenceInSeconds(
d476bc1b
JB
1028 interval.end,
1029 interval.start,
710d50eb 1030 )}`,
d476bc1b 1031 );
55f2ab60 1032 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
d476bc1b
JB
1033 }
1034};
1035
fba11dc6
JB
1036const getRandomSerialNumberSuffix = (params?: {
1037 randomBytesLength?: number;
1038 upperCase?: boolean;
1039}): string => {
1040 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex');
1041 if (params?.upperCase) {
1042 return randomSerialNumberSuffix.toUpperCase();
17ac262c 1043 }
fba11dc6
JB
1044 return randomSerialNumberSuffix;
1045};