refactor: cleanup RFID authorization code
[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 &&
829 chargingSchedule.duration! >
830 differenceInSeconds(
975e18ec
JB
831 addSeconds(
832 chargingSchedule.startSchedule!,
833 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
834 ),
d9dc6292 835 chargingSchedule.startSchedule!,
975e18ec 836 ))
252a7d22
JB
837 ) {
838 const result: ChargingProfilesLimit = {
e3037969 839 limit: previousChargingSchedulePeriod.limit,
bbb55ee4 840 chargingProfile: chargingProfile,
252a7d22
JB
841 };
842 logger.debug(debugLogMsg, result);
843 return result;
844 }
17ac262c
JB
845 }
846 }
847 }
17ac262c 848 }
fba11dc6 849};
17ac262c 850
0eb666db
JB
851export const prepareChargingProfileKind = (
852 connectorStatus: ConnectorStatus,
853 chargingProfile: ChargingProfile,
854 currentDate: Date,
855 logPrefix: string,
856): boolean => {
857 switch (chargingProfile.chargingProfileKind) {
858 case ChargingProfileKindType.RECURRING:
859 if (!canProceedRecurringChargingProfile(chargingProfile, logPrefix)) {
860 return false;
861 }
862 prepareRecurringChargingProfile(chargingProfile, currentDate, logPrefix);
863 break;
864 case ChargingProfileKindType.RELATIVE:
865 connectorStatus?.transactionStarted &&
866 (chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart);
867 break;
868 }
869 return true;
870};
871
ad490d5f 872export const canProceedChargingProfile = (
0bd926c1
JB
873 chargingProfile: ChargingProfile,
874 currentDate: Date,
875 logPrefix: string,
876): boolean => {
877 if (
878 (isValidTime(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom!)) ||
879 (isValidTime(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo!))
880 ) {
881 logger.debug(
882 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
883 chargingProfile.chargingProfileId
884 } is not valid for the current date ${currentDate.toISOString()}`,
885 );
886 return false;
887 }
888 const chargingSchedule = chargingProfile.chargingSchedule;
889 if (isNullOrUndefined(chargingSchedule?.startSchedule)) {
890 logger.error(
ad490d5f 891 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined`,
0bd926c1
JB
892 );
893 return false;
894 }
895 if (isNullOrUndefined(chargingSchedule?.duration)) {
896 logger.error(
897 `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined, not yet supported`,
898 );
899 return false;
900 }
901 return true;
902};
903
0eb666db 904const canProceedRecurringChargingProfile = (
0bd926c1
JB
905 chargingProfile: ChargingProfile,
906 logPrefix: string,
907): boolean => {
908 if (
909 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
910 isNullOrUndefined(chargingProfile.recurrencyKind)
911 ) {
912 logger.error(
913 `${logPrefix} ${moduleName}.canProceedRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} has no recurrencyKind defined`,
914 );
915 return false;
916 }
917 return true;
918};
919
522e4b05 920/**
ec4a242a 921 * Adjust recurring charging profile startSchedule to the current recurrency time interval if needed
522e4b05
JB
922 *
923 * @param chargingProfile -
924 * @param currentDate -
925 * @param logPrefix -
926 */
0eb666db 927const prepareRecurringChargingProfile = (
76dab5a9
JB
928 chargingProfile: ChargingProfile,
929 currentDate: Date,
930 logPrefix: string,
ec4a242a 931): boolean => {
76dab5a9 932 const chargingSchedule = chargingProfile.chargingSchedule;
ec4a242a 933 let recurringIntervalTranslated = false;
522e4b05 934 let recurringInterval: Interval;
76dab5a9
JB
935 switch (chargingProfile.recurrencyKind) {
936 case RecurrencyKindType.DAILY:
522e4b05
JB
937 recurringInterval = {
938 start: chargingSchedule.startSchedule!,
939 end: addDays(chargingSchedule.startSchedule!, 1),
940 };
d476bc1b 941 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
522e4b05
JB
942 if (
943 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 944 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
945 ) {
946 chargingSchedule.startSchedule = addDays(
991fb26b 947 recurringInterval.start,
05b52716 948 differenceInDays(currentDate, recurringInterval.start),
76dab5a9 949 );
522e4b05
JB
950 recurringInterval = {
951 start: chargingSchedule.startSchedule,
952 end: addDays(chargingSchedule.startSchedule, 1),
953 };
ec4a242a 954 recurringIntervalTranslated = true;
76dab5a9
JB
955 }
956 break;
957 case RecurrencyKindType.WEEKLY:
522e4b05
JB
958 recurringInterval = {
959 start: chargingSchedule.startSchedule!,
960 end: addWeeks(chargingSchedule.startSchedule!, 1),
961 };
d476bc1b 962 checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix);
522e4b05
JB
963 if (
964 !isWithinInterval(currentDate, recurringInterval) &&
991fb26b 965 isBefore(recurringInterval.end, currentDate)
522e4b05
JB
966 ) {
967 chargingSchedule.startSchedule = addWeeks(
991fb26b 968 recurringInterval.start,
05b52716 969 differenceInWeeks(currentDate, recurringInterval.start),
76dab5a9 970 );
522e4b05
JB
971 recurringInterval = {
972 start: chargingSchedule.startSchedule,
973 end: addWeeks(chargingSchedule.startSchedule, 1),
974 };
ec4a242a 975 recurringIntervalTranslated = true;
76dab5a9
JB
976 }
977 break;
ec4a242a
JB
978 default:
979 logger.error(
980 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring charging profile id ${chargingProfile.chargingProfileId} recurrency kind ${chargingProfile.recurrencyKind} is not supported`,
981 );
76dab5a9 982 }
ec4a242a 983 if (recurringIntervalTranslated && !isWithinInterval(currentDate, recurringInterval!)) {
522e4b05 984 logger.error(
aa5c5ad4 985 `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
522e4b05 986 chargingProfile.recurrencyKind
991fb26b 987 } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
522e4b05 988 recurringInterval!.start,
991fb26b
JB
989 ).toISOString()}, ${toDate(
990 recurringInterval!.end,
ec4a242a 991 ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `,
522e4b05
JB
992 );
993 }
ec4a242a 994 return recurringIntervalTranslated;
76dab5a9
JB
995};
996
d476bc1b
JB
997const checkRecurringChargingProfileDuration = (
998 chargingProfile: ChargingProfile,
999 interval: Interval,
1000 logPrefix: string,
ec4a242a 1001): void => {
142a66c9
JB
1002 if (isNullOrUndefined(chargingProfile.chargingSchedule.duration)) {
1003 logger.warn(
1004 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
1005 chargingProfile.chargingProfileKind
1006 } charging profile id ${
1007 chargingProfile.chargingProfileId
1008 } duration is not defined, set it to the recurrency time interval duration ${differenceInSeconds(
1009 interval.end,
1010 interval.start,
1011 )}`,
1012 );
1013 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
1014 } else if (
d476bc1b
JB
1015 chargingProfile.chargingSchedule.duration! > differenceInSeconds(interval.end, interval.start)
1016 ) {
1017 logger.warn(
aa5c5ad4 1018 `${logPrefix} ${moduleName}.checkRecurringChargingProfileDuration: Recurring ${
d476bc1b
JB
1019 chargingProfile.chargingProfileKind
1020 } charging profile id ${chargingProfile.chargingProfileId} duration ${
1021 chargingProfile.chargingSchedule.duration
710d50eb 1022 } is greater than the recurrency time interval duration ${differenceInSeconds(
d476bc1b
JB
1023 interval.end,
1024 interval.start,
710d50eb 1025 )}`,
d476bc1b 1026 );
55f2ab60 1027 chargingProfile.chargingSchedule.duration = differenceInSeconds(interval.end, interval.start);
d476bc1b
JB
1028 }
1029};
1030
fba11dc6
JB
1031const getRandomSerialNumberSuffix = (params?: {
1032 randomBytesLength?: number;
1033 upperCase?: boolean;
1034}): string => {
1035 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex');
1036 if (params?.upperCase) {
1037 return randomSerialNumberSuffix.toUpperCase();
17ac262c 1038 }
fba11dc6
JB
1039 return randomSerialNumberSuffix;
1040};