refactor: factor out charging profiles preparation
[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 => {
e1d9a0f4 521 let limit: number | undefined, matchingChargingProfile: 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;
537 matchingChargingProfile = result?.matchingChargingProfile;
538 switch (chargingStation.getCurrentOutType()) {
539 case CurrentType.AC:
540 limit =
e1d9a0f4
JB
541 matchingChargingProfile?.chargingSchedule?.chargingRateUnit ===
542 ChargingRateUnitType.WATT
fba11dc6
JB
543 ? limit
544 : ACElectricUtils.powerTotal(
545 chargingStation.getNumberOfPhases(),
546 chargingStation.getVoltageOut(),
e1d9a0f4 547 limit!,
fba11dc6
JB
548 );
549 break;
550 case CurrentType.DC:
551 limit =
e1d9a0f4
JB
552 matchingChargingProfile?.chargingSchedule?.chargingRateUnit ===
553 ChargingRateUnitType.WATT
fba11dc6 554 ? limit
e1d9a0f4 555 : DCElectricUtils.power(chargingStation.getVoltageOut(), limit!);
fba11dc6
JB
556 }
557 const connectorMaximumPower =
558 chargingStation.getMaximumPower() / chargingStation.powerDivider;
e1d9a0f4 559 if (limit! > connectorMaximumPower) {
fba11dc6 560 logger.error(
aa5c5ad4 561 `${chargingStation.logPrefix()} ${moduleName}.getChargingStationConnectorChargingProfilesPowerLimit: Charging profile id ${matchingChargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
5edd8ba0 562 result,
fba11dc6
JB
563 );
564 limit = connectorMaximumPower;
15068be9
JB
565 }
566 }
15068be9 567 }
fba11dc6
JB
568 return limit;
569};
570
571export const getDefaultVoltageOut = (
572 currentType: CurrentType,
573 logPrefix: string,
5edd8ba0 574 templateFile: string,
fba11dc6
JB
575): Voltage => {
576 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
577 let defaultVoltageOut: number;
578 switch (currentType) {
579 case CurrentType.AC:
580 defaultVoltageOut = Voltage.VOLTAGE_230;
581 break;
582 case CurrentType.DC:
583 defaultVoltageOut = Voltage.VOLTAGE_400;
584 break;
585 default:
586 logger.error(`${logPrefix} ${errorMsg}`);
587 throw new BaseError(errorMsg);
15068be9 588 }
fba11dc6
JB
589 return defaultVoltageOut;
590};
591
592export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
593 return (
594 stationInfo.idTagsFile &&
595 join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
596 );
597};
598
b2b60626 599export const waitChargingStationEvents = async (
fba11dc6
JB
600 emitter: EventEmitter,
601 event: ChargingStationWorkerMessageEvents,
5edd8ba0 602 eventsToWait: number,
fba11dc6 603): Promise<number> => {
474d4ffc 604 return new Promise<number>((resolve) => {
fba11dc6
JB
605 let events = 0;
606 if (eventsToWait === 0) {
607 resolve(events);
608 }
609 emitter.on(event, () => {
610 ++events;
611 if (events === eventsToWait) {
b1f1b0f6
JB
612 resolve(events);
613 }
b1f1b0f6 614 });
fba11dc6
JB
615 });
616};
617
cfc9875a
JB
618const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
619 let configuredMaxNumberOfConnectors = 0;
fba11dc6
JB
620 if (isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
621 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
cfc9875a 622 configuredMaxNumberOfConnectors =
fba11dc6
JB
623 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)];
624 } else if (isUndefined(stationTemplate.numberOfConnectors) === false) {
cfc9875a 625 configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors as number;
fba11dc6 626 } else if (stationTemplate.Connectors && !stationTemplate.Evses) {
cfc9875a 627 configuredMaxNumberOfConnectors = stationTemplate.Connectors[0]
fba11dc6
JB
628 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
629 : getMaxNumberOfConnectors(stationTemplate.Connectors);
630 } else if (stationTemplate.Evses && !stationTemplate.Connectors) {
fba11dc6
JB
631 for (const evse in stationTemplate.Evses) {
632 if (evse === '0') {
633 continue;
cda5d0fb 634 }
cfc9875a
JB
635 configuredMaxNumberOfConnectors += getMaxNumberOfConnectors(
636 stationTemplate.Evses[evse].Connectors,
637 );
cda5d0fb 638 }
cda5d0fb 639 }
cfc9875a 640 return configuredMaxNumberOfConnectors;
fba11dc6
JB
641};
642
643const checkConfiguredMaxConnectors = (
644 configuredMaxConnectors: number,
645 logPrefix: string,
5edd8ba0 646 templateFile: string,
fba11dc6
JB
647): void => {
648 if (configuredMaxConnectors <= 0) {
649 logger.warn(
5edd8ba0 650 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
fba11dc6 651 );
cda5d0fb 652 }
fba11dc6 653};
cda5d0fb 654
fba11dc6
JB
655const checkTemplateMaxConnectors = (
656 templateMaxConnectors: number,
657 logPrefix: string,
5edd8ba0 658 templateFile: string,
fba11dc6
JB
659): void => {
660 if (templateMaxConnectors === 0) {
661 logger.warn(
5edd8ba0 662 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
fba11dc6
JB
663 );
664 } else if (templateMaxConnectors < 0) {
665 logger.error(
5edd8ba0 666 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
fba11dc6
JB
667 );
668 }
669};
670
671const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
672 connectorStatus.availability = AvailabilityType.Operative;
673 connectorStatus.idTagLocalAuthorized = false;
674 connectorStatus.idTagAuthorized = false;
675 connectorStatus.transactionRemoteStarted = false;
676 connectorStatus.transactionStarted = false;
677 connectorStatus.energyActiveImportRegisterValue = 0;
678 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
679 if (isUndefined(connectorStatus.chargingProfiles)) {
680 connectorStatus.chargingProfiles = [];
681 }
682};
683
684const warnDeprecatedTemplateKey = (
685 template: ChargingStationTemplate,
686 key: string,
687 logPrefix: string,
688 templateFile: string,
5edd8ba0 689 logMsgToAppend = '',
fba11dc6 690): void => {
a37fc6dc 691 if (!isUndefined(template[key as keyof ChargingStationTemplate])) {
fba11dc6
JB
692 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
693 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
694 }`;
695 logger.warn(`${logPrefix} ${logMsg}`);
696 console.warn(chalk.yellow(`${logMsg}`));
697 }
698};
699
700const convertDeprecatedTemplateKey = (
701 template: ChargingStationTemplate,
702 deprecatedKey: string,
e1d9a0f4 703 key?: string,
fba11dc6 704): void => {
a37fc6dc 705 if (!isUndefined(template[deprecatedKey as keyof ChargingStationTemplate])) {
e1d9a0f4 706 if (!isUndefined(key)) {
a37fc6dc
JB
707 (template as unknown as Record<string, unknown>)[key!] =
708 template[deprecatedKey as keyof ChargingStationTemplate];
e1d9a0f4 709 }
a37fc6dc 710 delete template[deprecatedKey as keyof ChargingStationTemplate];
fba11dc6
JB
711 }
712};
713
947f048a
JB
714interface ChargingProfilesLimit {
715 limit: number;
716 matchingChargingProfile: ChargingProfile;
717}
718
fba11dc6 719/**
d467756c 720 * Charging profiles shall already be sorted by connector id and stack level (highest stack level has priority)
fba11dc6 721 *
d467756c
JB
722 * @param chargingStation -
723 * @param connectorId -
fba11dc6
JB
724 * @param chargingProfiles -
725 * @param logPrefix -
947f048a 726 * @returns ChargingProfilesLimit
fba11dc6
JB
727 */
728const getLimitFromChargingProfiles = (
a71d4e70
JB
729 chargingStation: ChargingStation,
730 connectorId: number,
fba11dc6 731 chargingProfiles: ChargingProfile[],
5edd8ba0 732 logPrefix: string,
947f048a 733): ChargingProfilesLimit | undefined => {
fba11dc6 734 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
fba11dc6 735 const currentDate = new Date();
0eb666db 736 const connectorStatus = chargingStation.getConnectorStatus(connectorId)!;
ad490d5f
JB
737 if (!isArraySorted(chargingProfiles, (a, b) => b.stackLevel - a.stackLevel)) {
738 logger.warn(
739 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profiles are not sorted by stack level. Trying to sort them`,
740 );
741 chargingProfiles.sort((a, b) => b.stackLevel - a.stackLevel);
742 }
fba11dc6 743 for (const chargingProfile of chargingProfiles) {
fba11dc6 744 const chargingSchedule = chargingProfile.chargingSchedule;
5543b88d 745 if (connectorStatus?.transactionStarted && isNullOrUndefined(chargingSchedule?.startSchedule)) {
109c677a 746 logger.debug(
ec4a242a 747 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`,
cda5d0fb 748 );
a71d4e70 749 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
b5c19509 750 chargingSchedule.startSchedule = connectorStatus?.transactionStart;
52952bf8 751 }
0bd926c1 752 if (!isDate(chargingSchedule?.startSchedule)) {
8d75a403 753 logger.warn(
ec4a242a 754 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date object. Trying to convert it to a Date object`,
8d75a403 755 );
991fb26b 756 chargingSchedule.startSchedule = convertToDate(chargingSchedule?.startSchedule)!;
8d75a403 757 }
0eb666db
JB
758 if (!prepareChargingProfileKind(connectorStatus, chargingProfile, currentDate, logPrefix)) {
759 continue;
ec4a242a 760 }
0bd926c1 761 if (!canProceedChargingProfile(chargingProfile, currentDate, logPrefix)) {
142a66c9
JB
762 continue;
763 }
fba11dc6
JB
764 // Check if the charging profile is active
765 if (
0bd926c1 766 isValidTime(chargingSchedule?.startSchedule) &&
975e18ec
JB
767 isWithinInterval(currentDate, {
768 start: chargingSchedule.startSchedule!,
769 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
770 })
fba11dc6 771 ) {
252a7d22 772 if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
80c58041
JB
773 const chargingSchedulePeriodCompareFn = (
774 a: ChargingSchedulePeriod,
775 b: ChargingSchedulePeriod,
776 ) => a.startPeriod - b.startPeriod;
777 if (
778 isArraySorted<ChargingSchedulePeriod>(
779 chargingSchedule.chargingSchedulePeriod,
780 chargingSchedulePeriodCompareFn,
781 ) === false
782 ) {
783 logger.warn(
784 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} schedule periods are not sorted by start period`,
785 );
786 chargingSchedule.chargingSchedulePeriod.sort(chargingSchedulePeriodCompareFn);
787 }
975e18ec 788 // Check if the first schedule period start period is equal to 0
55f2ab60
JB
789 if (chargingSchedule.chargingSchedulePeriod[0].startPeriod !== 0) {
790 logger.error(
791 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} first schedule period start period ${chargingSchedule.chargingSchedulePeriod[0].startPeriod} is not equal to 0`,
792 );
975e18ec 793 continue;
55f2ab60 794 }
991fb26b 795 // Handle only one schedule period
975e18ec 796 if (chargingSchedule.chargingSchedulePeriod.length === 1) {
252a7d22
JB
797 const result: ChargingProfilesLimit = {
798 limit: chargingSchedule.chargingSchedulePeriod[0].limit,
fba11dc6
JB
799 matchingChargingProfile: chargingProfile,
800 };
801 logger.debug(debugLogMsg, result);
802 return result;
41189456 803 }
e3037969 804 let previousChargingSchedulePeriod: ChargingSchedulePeriod | undefined;
252a7d22 805 // Search for the right schedule period
e3037969
JB
806 for (const [
807 index,
808 chargingSchedulePeriod,
809 ] of chargingSchedule.chargingSchedulePeriod.entries()) {
252a7d22
JB
810 // Find the right schedule period
811 if (
812 isAfter(
e3037969 813 addSeconds(chargingSchedule.startSchedule!, chargingSchedulePeriod.startPeriod),
252a7d22
JB
814 currentDate,
815 )
816 ) {
e3037969 817 // Found the schedule period: previous is the correct one
252a7d22 818 const result: ChargingProfilesLimit = {
e3037969 819 limit: previousChargingSchedulePeriod!.limit,
252a7d22
JB
820 matchingChargingProfile: chargingProfile,
821 };
822 logger.debug(debugLogMsg, result);
823 return result;
824 }
e3037969
JB
825 // Keep a reference to previous one
826 previousChargingSchedulePeriod = chargingSchedulePeriod;
975e18ec 827 // Handle the last schedule period within the charging profile duration
252a7d22 828 if (
975e18ec
JB
829 index === chargingSchedule.chargingSchedulePeriod.length - 1 ||
830 (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
831 chargingSchedule.duration! >
832 differenceInSeconds(
975e18ec
JB
833 addSeconds(
834 chargingSchedule.startSchedule!,
835 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
836 ),
d9dc6292 837 chargingSchedule.startSchedule!,
975e18ec 838 ))
252a7d22
JB
839 ) {
840 const result: ChargingProfilesLimit = {
e3037969 841 limit: previousChargingSchedulePeriod.limit,
252a7d22
JB
842 matchingChargingProfile: chargingProfile,
843 };
844 logger.debug(debugLogMsg, result);
845 return result;
846 }
17ac262c
JB
847 }
848 }
849 }
17ac262c 850 }
fba11dc6 851};
17ac262c 852
0eb666db
JB
853export const prepareChargingProfileKind = (
854 connectorStatus: ConnectorStatus,
855 chargingProfile: ChargingProfile,
856 currentDate: Date,
857 logPrefix: string,
858): boolean => {
859 switch (chargingProfile.chargingProfileKind) {
860 case ChargingProfileKindType.RECURRING:
861 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
862 return false;
863 }
864 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix);
865 break;
866 case ChargingProfileKindType.RELATIVE:
867 connectorStatus?.transactionStarted &&
868 (chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart);
869 break;
870 }
871 return true;
872};
873
ad490d5f 874export const canProceedChargingProfile = (
0bd926c1
JB
875 chargingProfile: ChargingProfile,
876 currentDate: Date,
877 logPrefix: string,
878): boolean => {
879 if (
880 (isValidTime(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom!)) ||
881 (isValidTime(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo!))
882 ) {
883 logger.debug(
884 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
885 chargingProfile.chargingProfileId
886 } is not valid for the current date ${currentDate.toISOString()}`,
887 );
888 return false;
889 }
890 const chargingSchedule = chargingProfile.chargingSchedule;
891 if (isNullOrUndefined(chargingSchedule?.startSchedule)) {
892 logger.error(
ad490d5f 893 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`,
0bd926c1
JB
894 );
895 return false;
896 }
897 if (isNullOrUndefined(chargingSchedule?.duration)) {
898 logger.error(
899 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined, not yet supported`,
900 );
901 return false;
902 }
903 return true;
904};
905
0eb666db 906const canProceedRecurringChargingProfile = (
0bd926c1
JB
907 chargingProfile: ChargingProfile,
908 logPrefix: string,
909): boolean => {
910 if (
911 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
912 isNullOrUndefined(chargingProfile.recurrencyKind)
913 ) {
914 logger.error(
915 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
916 );
917 return false;
918 }
919 return true;
920};
921
522e4b05 922/**
ec4a242a 923 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
522e4b05
JB
924 *
925 * @param chargingProfile -
926 * @param currentDate -
927 * @param logPrefix -
928 */
0eb666db 929const prepareRecurringChargingProfile = (
76dab5a9
JB
930 chargingProfile: ChargingProfile,
931 currentDate: Date,
932 logPrefix: string,
ec4a242a 933): boolean => {
76dab5a9 934 const chargingSchedule = chargingProfile.chargingSchedule;
ec4a242a 935 let recurringIntervalTranslated = false;
522e4b05 936 let recurringInterval: Interval;
76dab5a9
JB
937 switch (chargingProfile.recurrencyKind) {
938 case RecurrencyKindType.DAILY:
522e4b05
JB
939 recurringInterval = {
940 start: chargingSchedule.startSchedule!,
941 end: addDays(chargingSchedule.startSchedule!, 1),
942 };
d476bc1b 943 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
522e4b05
JB
944 if (
945 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 946 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
947 ) {
948 chargingSchedule.startSchedule = addDays(
991fb26b 949 recurringInterval.start,
05b52716 950 differenceInDays(currentDate, recurringInterval.start),
76dab5a9 951 );
522e4b05
JB
952 recurringInterval = {
953 start: chargingSchedule.startSchedule,
954 end: addDays(chargingSchedule.startSchedule, 1),
955 };
ec4a242a 956 recurringIntervalTranslated = true;
76dab5a9
JB
957 }
958 break;
959 case RecurrencyKindType.WEEKLY:
522e4b05
JB
960 recurringInterval = {
961 start: chargingSchedule.startSchedule!,
962 end: addWeeks(chargingSchedule.startSchedule!, 1),
963 };
d476bc1b 964 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
522e4b05
JB
965 if (
966 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 967 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
968 ) {
969 chargingSchedule.startSchedule = addWeeks(
991fb26b 970 recurringInterval.start,
05b52716 971 differenceInWeeks(currentDate, recurringInterval.start),
76dab5a9 972 );
522e4b05
JB
973 recurringInterval = {
974 start: chargingSchedule.startSchedule,
975 end: addWeeks(chargingSchedule.startSchedule, 1),
976 };
ec4a242a 977 recurringIntervalTranslated = true;
76dab5a9
JB
978 }
979 break;
ec4a242a
JB
980 default:
981 logger.error(
982 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} recurrency kind ${chargingProfile.recurrencyKind} is not supported`,
983 );
76dab5a9 984 }
ec4a242a 985 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
522e4b05 986 logger.error(
aa5c5ad4 987 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
522e4b05 988 chargingProfile.recurrencyKind
991fb26b 989 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
522e4b05 990 recurringInterval!.start,
991fb26b
JB
991 ).toISOString()}, ${toDate(
992 recurringInterval!.end,
ec4a242a 993 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
522e4b05
JB
994 );
995 }
ec4a242a 996 return recurringIntervalTranslated;
76dab5a9
JB
997};
998
d476bc1b
JB
999const checkRecurringChargingProfileDuration = (
1000 chargingProfile: ChargingProfile,
1001 interval: Interval,
1002 logPrefix: string,
ec4a242a 1003): void => {
142a66c9
JB
1004 if (isNullOrUndefined(chargingProfile.chargingSchedule.duration)) {
1005 logger.warn(
1006 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1007 chargingProfile.chargingProfileKind
1008 } charging profile id ${
1009 chargingProfile.chargingProfileId
1010 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1011 interval.end,
1012 interval.start,
1013 )}`,
1014 );
1015 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
1016 } else if (
d476bc1b
JB
1017 chargingProfile.chargingSchedule.duration! > differenceInSeconds(interval.end, interval.start)
1018 ) {
1019 logger.warn(
aa5c5ad4 1020 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
d476bc1b
JB
1021 chargingProfile.chargingProfileKind
1022 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1023 chargingProfile.chargingSchedule.duration
710d50eb 1024 } is greater than the recurrency time interval duration ${differenceInSeconds(
d476bc1b
JB
1025 interval.end,
1026 interval.start,
710d50eb 1027 )}`,
d476bc1b 1028 );
55f2ab60 1029 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
d476bc1b
JB
1030 }
1031};
1032
fba11dc6
JB
1033const getRandomSerialNumberSuffix = (params?: {
1034 randomBytesLength?: number;
1035 upperCase?: boolean;
1036}): string => {
1037 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex');
1038 if (params?.upperCase) {
1039 return randomSerialNumberSuffix.toUpperCase();
17ac262c 1040 }
fba11dc6
JB
1041 return randomSerialNumberSuffix;
1042};