build(deps): apply updates
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStationUtils.ts
CommitLineData
43ee4373 1import crypto from 'node:crypto';
130783a7
JB
2import path from 'node:path';
3import { fileURLToPath } from 'node:url';
8114d10e 4
e302df1d 5import chalk from 'chalk';
8114d10e
JB
6import moment from 'moment';
7
2896e06d 8import type { ChargingStation } from './internal';
268a74bb 9import { BaseError } from '../exception';
981ebfbe 10import {
492cf6ab 11 AmpereUnits,
268a74bb
JB
12 type BootNotificationRequest,
13 BootReasonEnumType,
15068be9 14 type ChargingProfile,
268a74bb 15 ChargingProfileKindType,
15068be9
JB
16 ChargingRateUnitType,
17 type ChargingSchedulePeriod,
268a74bb
JB
18 type ChargingStationInfo,
19 type ChargingStationTemplate,
20 CurrentType,
21 type OCPP16BootNotificationRequest,
22 type OCPP20BootNotificationRequest,
23 OCPPVersion,
24 RecurrencyKindType,
25 Voltage,
26} from '../types';
60a74391
JB
27import {
28 ACElectricUtils,
29 Configuration,
30 Constants,
31 DCElectricUtils,
32 Utils,
33 logger,
34} from '../utils';
268a74bb 35import { WorkerProcessType } from '../worker';
17ac262c 36
91a4f151
JB
37const moduleName = 'ChargingStationUtils';
38
17ac262c 39export class ChargingStationUtils {
d5bd1c00
JB
40 private constructor() {
41 // This is intentional
42 }
43
17ac262c
JB
44 public static getChargingStationId(
45 index: number,
46 stationTemplate: ChargingStationTemplate
47 ): string {
48 // In case of multiple instances: add instance index to charging station id
49 const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
1b271a54 50 const idSuffix = stationTemplate?.nameSuffix ?? '';
44eb6026 51 const idStr = `000000000${index.toString()}`;
ccb1d6e9 52 return stationTemplate?.fixedName
17ac262c 53 ? stationTemplate.baseName
44eb6026
JB
54 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
55 idStr.length - 4
56 )}${idSuffix}`;
17ac262c
JB
57 }
58
fa7bccf4 59 public static getHashId(index: number, stationTemplate: ChargingStationTemplate): string {
99e92237 60 const chargingStationInfo = {
fa7bccf4
JB
61 chargePointModel: stationTemplate.chargePointModel,
62 chargePointVendor: stationTemplate.chargePointVendor,
63 ...(!Utils.isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && {
64 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix,
17ac262c 65 }),
fa7bccf4
JB
66 ...(!Utils.isUndefined(stationTemplate.chargePointSerialNumberPrefix) && {
67 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix,
17ac262c 68 }),
fa7bccf4
JB
69 ...(!Utils.isUndefined(stationTemplate.meterSerialNumberPrefix) && {
70 meterSerialNumber: stationTemplate.meterSerialNumberPrefix,
17ac262c 71 }),
fa7bccf4
JB
72 ...(!Utils.isUndefined(stationTemplate.meterType) && {
73 meterType: stationTemplate.meterType,
17ac262c
JB
74 }),
75 };
76 return crypto
77 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
fa7bccf4 78 .update(
14ecae6a
JB
79 `${JSON.stringify(chargingStationInfo)}${ChargingStationUtils.getChargingStationId(
80 index,
81 stationTemplate
82 )}`
fa7bccf4 83 )
17ac262c
JB
84 .digest('hex');
85 }
86
1bf29f5b
JB
87 public static checkChargingStation(chargingStation: ChargingStation, logPrefix: string): boolean {
88 if (chargingStation.started === false && chargingStation.starting === false) {
89 logger.warn(`${logPrefix} charging station is stopped, cannot proceed`);
90 return false;
91 }
92 return true;
93 }
94
fa7bccf4
JB
95 public static getTemplateMaxNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
96 const templateConnectors = stationTemplate?.Connectors;
97 if (!templateConnectors) {
98 return -1;
99 }
100 return Object.keys(templateConnectors).length;
101 }
102
103 public static checkTemplateMaxConnectors(
104 templateMaxConnectors: number,
105 templateFile: string,
106 logPrefix: string
107 ): void {
108 if (templateMaxConnectors === 0) {
109 logger.warn(
110 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
111 );
112 } else if (templateMaxConnectors < 0) {
113 logger.error(
114 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
115 );
116 }
117 }
118
c72f6634 119 public static getConfiguredNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
fa7bccf4 120 let configuredMaxConnectors: number;
53ac516c 121 if (Utils.isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
fa7bccf4 122 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
c72f6634
JB
123 configuredMaxConnectors =
124 numberOfConnectors[Math.floor(Utils.secureRandom() * numberOfConnectors.length)];
125 } else if (Utils.isUndefined(stationTemplate.numberOfConnectors) === false) {
fa7bccf4
JB
126 configuredMaxConnectors = stationTemplate.numberOfConnectors as number;
127 } else {
128 configuredMaxConnectors = stationTemplate?.Connectors[0]
129 ? ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate) - 1
130 : ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate);
131 }
132 return configuredMaxConnectors;
133 }
134
135 public static checkConfiguredMaxConnectors(
136 configuredMaxConnectors: number,
137 templateFile: string,
138 logPrefix: string
139 ): void {
140 if (configuredMaxConnectors <= 0) {
141 logger.warn(
142 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
143 );
144 }
145 }
146
17ac262c 147 public static createBootNotificationRequest(
15068be9
JB
148 stationInfo: ChargingStationInfo,
149 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
17ac262c 150 ): BootNotificationRequest {
d270cc87
JB
151 const ocppVersion = stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
152 switch (ocppVersion) {
153 case OCPPVersion.VERSION_16:
154 return {
155 chargePointModel: stationInfo.chargePointModel,
156 chargePointVendor: stationInfo.chargePointVendor,
157 ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && {
158 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber,
159 }),
160 ...(!Utils.isUndefined(stationInfo.chargePointSerialNumber) && {
161 chargePointSerialNumber: stationInfo.chargePointSerialNumber,
162 }),
163 ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
164 firmwareVersion: stationInfo.firmwareVersion,
165 }),
166 ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
167 ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
168 ...(!Utils.isUndefined(stationInfo.meterSerialNumber) && {
169 meterSerialNumber: stationInfo.meterSerialNumber,
170 }),
171 ...(!Utils.isUndefined(stationInfo.meterType) && {
172 meterType: stationInfo.meterType,
173 }),
174 } as OCPP16BootNotificationRequest;
175 case OCPPVersion.VERSION_20:
176 case OCPPVersion.VERSION_201:
177 return {
15068be9 178 reason: bootReason,
d270cc87
JB
179 chargingStation: {
180 model: stationInfo.chargePointModel,
181 vendorName: stationInfo.chargePointVendor,
182 ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
183 firmwareVersion: stationInfo.firmwareVersion,
184 }),
185 ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && {
186 serialNumber: stationInfo.chargeBoxSerialNumber,
187 }),
98fc1389
JB
188 ...((!Utils.isUndefined(stationInfo.iccid) || !Utils.isUndefined(stationInfo.imsi)) && {
189 modem: {
190 ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
191 ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
192 },
193 }),
d270cc87
JB
194 },
195 } as OCPP20BootNotificationRequest;
196 }
17ac262c
JB
197 }
198
199 public static workerPoolInUse(): boolean {
721646e9 200 return [WorkerProcessType.dynamicPool, WorkerProcessType.staticPool].includes(
cf2a5d9b 201 Configuration.getWorker().processType
17ac262c
JB
202 );
203 }
204
205 public static workerDynamicPoolInUse(): boolean {
721646e9 206 return Configuration.getWorker().processType === WorkerProcessType.dynamicPool;
17ac262c
JB
207 }
208
ae5020a3 209 public static warnTemplateKeysDeprecation(
17ac262c 210 templateFile: string,
ae5020a3
JB
211 stationTemplate: ChargingStationTemplate,
212 logPrefix: string
213 ) {
214 const templateKeys: { key: string; deprecatedKey: string }[] = [
215 { key: 'supervisionUrls', deprecatedKey: 'supervisionUrl' },
216 { key: 'idTagsFile', deprecatedKey: 'authorizationFile' },
217 ];
218 for (const templateKey of templateKeys) {
219 ChargingStationUtils.warnDeprecatedTemplateKey(
220 stationTemplate,
221 templateKey.deprecatedKey,
222 templateFile,
223 logPrefix,
224 `Use '${templateKey.key}' instead`
225 );
226 ChargingStationUtils.convertDeprecatedTemplateKey(
227 stationTemplate,
228 templateKey.deprecatedKey,
229 templateKey.key
230 );
17ac262c
JB
231 }
232 }
233
fa7bccf4
JB
234 public static stationTemplateToStationInfo(
235 stationTemplate: ChargingStationTemplate
236 ): ChargingStationInfo {
237 stationTemplate = Utils.cloneObject(stationTemplate);
238 delete stationTemplate.power;
239 delete stationTemplate.powerUnit;
240 delete stationTemplate.Configuration;
241 delete stationTemplate.AutomaticTransactionGenerator;
242 delete stationTemplate.chargeBoxSerialNumberPrefix;
243 delete stationTemplate.chargePointSerialNumberPrefix;
fec4d204 244 delete stationTemplate.meterSerialNumberPrefix;
51c83d6f 245 return stationTemplate as unknown as ChargingStationInfo;
fa7bccf4
JB
246 }
247
248 public static createStationInfoHash(stationInfo: ChargingStationInfo): void {
ccb1d6e9 249 delete stationInfo.infoHash;
7c72977b 250 stationInfo.infoHash = crypto
ccb1d6e9
JB
251 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
252 .update(JSON.stringify(stationInfo))
253 .digest('hex');
17ac262c
JB
254 }
255
256 public static createSerialNumber(
fa7bccf4 257 stationTemplate: ChargingStationTemplate,
4d20f040 258 stationInfo: ChargingStationInfo,
fa7bccf4
JB
259 params: {
260 randomSerialNumberUpperCase?: boolean;
261 randomSerialNumber?: boolean;
262 } = {
17ac262c
JB
263 randomSerialNumberUpperCase: true,
264 randomSerialNumber: true,
265 }
266 ): void {
abe9e9dd 267 params = params ?? {};
17ac262c
JB
268 params.randomSerialNumberUpperCase = params?.randomSerialNumberUpperCase ?? true;
269 params.randomSerialNumber = params?.randomSerialNumber ?? true;
fa7bccf4
JB
270 const serialNumberSuffix = params?.randomSerialNumber
271 ? ChargingStationUtils.getRandomSerialNumberSuffix({
272 upperCase: params.randomSerialNumberUpperCase,
273 })
274 : '';
9a15316c
JB
275 stationInfo.chargePointSerialNumber = Utils.isNotEmptyString(
276 stationTemplate?.chargePointSerialNumberPrefix
277 )
278 ? `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`
279 : undefined;
280 stationInfo.chargeBoxSerialNumber = Utils.isNotEmptyString(
281 stationTemplate?.chargeBoxSerialNumberPrefix
282 )
283 ? `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`
284 : undefined;
285 stationInfo.meterSerialNumber = Utils.isNotEmptyString(stationTemplate?.meterSerialNumberPrefix)
286 ? `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`
287 : undefined;
fec4d204
JB
288 }
289
290 public static propagateSerialNumber(
291 stationTemplate: ChargingStationTemplate,
292 stationInfoSrc: ChargingStationInfo,
4d20f040 293 stationInfoDst: ChargingStationInfo
fec4d204
JB
294 ) {
295 if (!stationInfoSrc || !stationTemplate) {
baf93dda
JB
296 throw new BaseError(
297 'Missing charging station template or existing configuration to propagate serial number'
298 );
fec4d204
JB
299 }
300 stationTemplate?.chargePointSerialNumberPrefix && stationInfoSrc?.chargePointSerialNumber
301 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
302 : stationInfoDst?.chargePointSerialNumber && delete stationInfoDst.chargePointSerialNumber;
303 stationTemplate?.chargeBoxSerialNumberPrefix && stationInfoSrc?.chargeBoxSerialNumber
304 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
305 : stationInfoDst?.chargeBoxSerialNumber && delete stationInfoDst.chargeBoxSerialNumber;
306 stationTemplate?.meterSerialNumberPrefix && stationInfoSrc?.meterSerialNumber
307 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
308 : stationInfoDst?.meterSerialNumber && delete stationInfoDst.meterSerialNumber;
17ac262c
JB
309 }
310
311 public static getAmperageLimitationUnitDivider(stationInfo: ChargingStationInfo): number {
312 let unitDivider = 1;
313 switch (stationInfo.amperageLimitationUnit) {
314 case AmpereUnits.DECI_AMPERE:
315 unitDivider = 10;
316 break;
317 case AmpereUnits.CENTI_AMPERE:
318 unitDivider = 100;
319 break;
320 case AmpereUnits.MILLI_AMPERE:
321 unitDivider = 1000;
322 break;
323 }
324 return unitDivider;
325 }
326
15068be9
JB
327 public static getChargingStationConnectorChargingProfilesPowerLimit(
328 chargingStation: ChargingStation,
329 connectorId: number
330 ): number | undefined {
331 let limit: number, matchingChargingProfile: ChargingProfile;
15068be9 332 // Get charging profiles for connector and sort by stack level
66a62eac
JB
333 const chargingProfiles =
334 Utils.cloneObject(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)?.sort(
335 (a, b) => b.stackLevel - a.stackLevel
336 ) ?? [];
15068be9 337 // Get profiles on connector 0
1895299d 338 if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
15068be9 339 chargingProfiles.push(
66a62eac
JB
340 ...Utils.cloneObject(chargingStation.getConnectorStatus(0).chargingProfiles).sort(
341 (a, b) => b.stackLevel - a.stackLevel
342 )
15068be9
JB
343 );
344 }
53ac516c 345 if (Utils.isNotEmptyArray(chargingProfiles)) {
15068be9
JB
346 const result = ChargingStationUtils.getLimitFromChargingProfiles(
347 chargingProfiles,
348 chargingStation.logPrefix()
349 );
350 if (!Utils.isNullOrUndefined(result)) {
1895299d
JB
351 limit = result?.limit;
352 matchingChargingProfile = result?.matchingChargingProfile;
15068be9
JB
353 switch (chargingStation.getCurrentOutType()) {
354 case CurrentType.AC:
355 limit =
356 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
357 ChargingRateUnitType.WATT
358 ? limit
359 : ACElectricUtils.powerTotal(
360 chargingStation.getNumberOfPhases(),
361 chargingStation.getVoltageOut(),
362 limit
363 );
364 break;
365 case CurrentType.DC:
366 limit =
367 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
368 ChargingRateUnitType.WATT
369 ? limit
370 : DCElectricUtils.power(chargingStation.getVoltageOut(), limit);
371 }
372 const connectorMaximumPower =
373 chargingStation.getMaximumPower() / chargingStation.powerDivider;
374 if (limit > connectorMaximumPower) {
375 logger.error(
376 `${chargingStation.logPrefix()} Charging profile id ${
377 matchingChargingProfile.chargingProfileId
378 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
379 result
380 );
381 limit = connectorMaximumPower;
382 }
383 }
384 }
385 return limit;
386 }
387
388 public static getDefaultVoltageOut(
389 currentType: CurrentType,
390 templateFile: string,
391 logPrefix: string
392 ): Voltage {
393 const errMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
394 let defaultVoltageOut: number;
395 switch (currentType) {
396 case CurrentType.AC:
397 defaultVoltageOut = Voltage.VOLTAGE_230;
398 break;
399 case CurrentType.DC:
400 defaultVoltageOut = Voltage.VOLTAGE_400;
401 break;
402 default:
403 logger.error(`${logPrefix} ${errMsg}`);
404 throw new BaseError(errMsg);
405 }
406 return defaultVoltageOut;
407 }
408
e302df1d 409 public static getIdTagsFile(stationInfo: ChargingStationInfo): string | undefined {
15068be9 410 return (
e302df1d 411 stationInfo.idTagsFile &&
15068be9
JB
412 path.join(
413 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
414 'assets',
e302df1d 415 path.basename(stationInfo.idTagsFile)
15068be9
JB
416 )
417 );
418 }
419
ae5020a3
JB
420 private static warnDeprecatedTemplateKey(
421 template: ChargingStationTemplate,
422 key: string,
423 templateFile: string,
424 logPrefix: string,
425 logMsgToAppend = ''
426 ): void {
427 if (!Utils.isUndefined(template[key])) {
428 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
429 Utils.isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
430 }`;
431 logger.warn(`${logPrefix} ${logMsg}`);
432 console.warn(chalk.yellow(`${logMsg}`));
433 }
434 }
435
436 private static convertDeprecatedTemplateKey(
437 template: ChargingStationTemplate,
438 deprecatedKey: string,
439 key: string
440 ): void {
441 if (!Utils.isUndefined(template[deprecatedKey])) {
442 template[key] = template[deprecatedKey] as unknown;
443 delete template[deprecatedKey];
444 }
445 }
446
17ac262c
JB
447 /**
448 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
449 *
0e4fa348
JB
450 * @param chargingProfiles -
451 * @param logPrefix -
452 * @returns
17ac262c 453 */
15068be9 454 private static getLimitFromChargingProfiles(
17ac262c
JB
455 chargingProfiles: ChargingProfile[],
456 logPrefix: string
457 ): {
458 limit: number;
459 matchingChargingProfile: ChargingProfile;
460 } | null {
91a4f151 461 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
41189456
JB
462 const currentMoment = moment();
463 const currentDate = new Date();
17ac262c
JB
464 for (const chargingProfile of chargingProfiles) {
465 // Set helpers
17ac262c 466 const chargingSchedule = chargingProfile.chargingSchedule;
41189456
JB
467 if (!chargingSchedule?.startSchedule) {
468 logger.warn(
469 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}`
470 );
471 }
17ac262c
JB
472 // Check type (recurring) and if it is already active
473 // Adjust the daily recurring schedule to today
474 if (
475 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
476 chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
477 currentMoment.isAfter(chargingSchedule.startSchedule)
478 ) {
41189456
JB
479 if (!(chargingSchedule?.startSchedule instanceof Date)) {
480 logger.warn(
481 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object`
482 );
483 chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule);
484 }
17ac262c
JB
485 chargingSchedule.startSchedule.setFullYear(
486 currentDate.getFullYear(),
487 currentDate.getMonth(),
488 currentDate.getDate()
489 );
490 // Check if the start of the schedule is yesterday
491 if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
492 chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
493 }
494 } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
495 return null;
496 }
497 // Check if the charging profile is active
498 if (
499 moment(chargingSchedule.startSchedule)
500 .add(chargingSchedule.duration, 's')
501 .isAfter(currentMoment)
502 ) {
503 let lastButOneSchedule: ChargingSchedulePeriod;
504 // Search the right schedule period
505 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
506 // Handling of only one period
507 if (
508 chargingSchedule.chargingSchedulePeriod.length === 1 &&
509 schedulePeriod.startPeriod === 0
510 ) {
511 const result = {
512 limit: schedulePeriod.limit,
513 matchingChargingProfile: chargingProfile,
514 };
91a4f151 515 logger.debug(debugLogMsg, result);
17ac262c
JB
516 return result;
517 }
518 // Find the right schedule period
519 if (
520 moment(chargingSchedule.startSchedule)
521 .add(schedulePeriod.startPeriod, 's')
522 .isAfter(currentMoment)
523 ) {
524 // Found the schedule: last but one is the correct one
525 const result = {
526 limit: lastButOneSchedule.limit,
527 matchingChargingProfile: chargingProfile,
528 };
91a4f151 529 logger.debug(debugLogMsg, result);
17ac262c
JB
530 return result;
531 }
532 // Keep it
533 lastButOneSchedule = schedulePeriod;
534 // Handle the last schedule period
535 if (
536 schedulePeriod.startPeriod ===
537 chargingSchedule.chargingSchedulePeriod[
538 chargingSchedule.chargingSchedulePeriod.length - 1
539 ].startPeriod
540 ) {
541 const result = {
542 limit: lastButOneSchedule.limit,
543 matchingChargingProfile: chargingProfile,
544 };
91a4f151 545 logger.debug(debugLogMsg, result);
17ac262c
JB
546 return result;
547 }
548 }
549 }
550 }
551 return null;
552 }
553
554 private static getRandomSerialNumberSuffix(params?: {
555 randomBytesLength?: number;
556 upperCase?: boolean;
557 }): string {
558 const randomSerialNumberSuffix = crypto
559 .randomBytes(params?.randomBytesLength ?? 16)
560 .toString('hex');
561 if (params?.upperCase) {
562 return randomSerialNumberSuffix.toUpperCase();
563 }
564 return randomSerialNumberSuffix;
565 }
566}