feat: add initial support get composite schedule OCPP 1.6 command
[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
41189456
JB
333 const chargingProfiles = Utils.cloneObject(
334 chargingStation
335 .getConnectorStatus(connectorId)
336 ?.chargingProfiles?.sort((a, b) => b.stackLevel - a.stackLevel) ?? []
337 );
15068be9 338 // Get profiles on connector 0
1895299d 339 if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
15068be9
JB
340 chargingProfiles.push(
341 ...chargingStation
342 .getConnectorStatus(0)
343 .chargingProfiles.sort((a, b) => b.stackLevel - a.stackLevel)
344 );
345 }
53ac516c 346 if (Utils.isNotEmptyArray(chargingProfiles)) {
15068be9
JB
347 const result = ChargingStationUtils.getLimitFromChargingProfiles(
348 chargingProfiles,
349 chargingStation.logPrefix()
350 );
351 if (!Utils.isNullOrUndefined(result)) {
1895299d
JB
352 limit = result?.limit;
353 matchingChargingProfile = result?.matchingChargingProfile;
15068be9
JB
354 switch (chargingStation.getCurrentOutType()) {
355 case CurrentType.AC:
356 limit =
357 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
358 ChargingRateUnitType.WATT
359 ? limit
360 : ACElectricUtils.powerTotal(
361 chargingStation.getNumberOfPhases(),
362 chargingStation.getVoltageOut(),
363 limit
364 );
365 break;
366 case CurrentType.DC:
367 limit =
368 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
369 ChargingRateUnitType.WATT
370 ? limit
371 : DCElectricUtils.power(chargingStation.getVoltageOut(), limit);
372 }
373 const connectorMaximumPower =
374 chargingStation.getMaximumPower() / chargingStation.powerDivider;
375 if (limit > connectorMaximumPower) {
376 logger.error(
377 `${chargingStation.logPrefix()} Charging profile id ${
378 matchingChargingProfile.chargingProfileId
379 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
380 result
381 );
382 limit = connectorMaximumPower;
383 }
384 }
385 }
386 return limit;
387 }
388
389 public static getDefaultVoltageOut(
390 currentType: CurrentType,
391 templateFile: string,
392 logPrefix: string
393 ): Voltage {
394 const errMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
395 let defaultVoltageOut: number;
396 switch (currentType) {
397 case CurrentType.AC:
398 defaultVoltageOut = Voltage.VOLTAGE_230;
399 break;
400 case CurrentType.DC:
401 defaultVoltageOut = Voltage.VOLTAGE_400;
402 break;
403 default:
404 logger.error(`${logPrefix} ${errMsg}`);
405 throw new BaseError(errMsg);
406 }
407 return defaultVoltageOut;
408 }
409
e302df1d 410 public static getIdTagsFile(stationInfo: ChargingStationInfo): string | undefined {
15068be9 411 return (
e302df1d 412 stationInfo.idTagsFile &&
15068be9
JB
413 path.join(
414 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
415 'assets',
e302df1d 416 path.basename(stationInfo.idTagsFile)
15068be9
JB
417 )
418 );
419 }
420
ae5020a3
JB
421 private static warnDeprecatedTemplateKey(
422 template: ChargingStationTemplate,
423 key: string,
424 templateFile: string,
425 logPrefix: string,
426 logMsgToAppend = ''
427 ): void {
428 if (!Utils.isUndefined(template[key])) {
429 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
430 Utils.isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
431 }`;
432 logger.warn(`${logPrefix} ${logMsg}`);
433 console.warn(chalk.yellow(`${logMsg}`));
434 }
435 }
436
437 private static convertDeprecatedTemplateKey(
438 template: ChargingStationTemplate,
439 deprecatedKey: string,
440 key: string
441 ): void {
442 if (!Utils.isUndefined(template[deprecatedKey])) {
443 template[key] = template[deprecatedKey] as unknown;
444 delete template[deprecatedKey];
445 }
446 }
447
17ac262c
JB
448 /**
449 * Charging profiles should already be sorted by connectorId and stack level (highest stack level has priority)
450 *
0e4fa348
JB
451 * @param chargingProfiles -
452 * @param logPrefix -
453 * @returns
17ac262c 454 */
15068be9 455 private static getLimitFromChargingProfiles(
17ac262c
JB
456 chargingProfiles: ChargingProfile[],
457 logPrefix: string
458 ): {
459 limit: number;
460 matchingChargingProfile: ChargingProfile;
461 } | null {
91a4f151 462 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
41189456
JB
463 const currentMoment = moment();
464 const currentDate = new Date();
17ac262c
JB
465 for (const chargingProfile of chargingProfiles) {
466 // Set helpers
17ac262c 467 const chargingSchedule = chargingProfile.chargingSchedule;
41189456
JB
468 if (!chargingSchedule?.startSchedule) {
469 logger.warn(
470 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}`
471 );
472 }
17ac262c
JB
473 // Check type (recurring) and if it is already active
474 // Adjust the daily recurring schedule to today
475 if (
476 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
477 chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
478 currentMoment.isAfter(chargingSchedule.startSchedule)
479 ) {
41189456
JB
480 if (!(chargingSchedule?.startSchedule instanceof Date)) {
481 logger.warn(
482 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object`
483 );
484 chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule);
485 }
17ac262c
JB
486 chargingSchedule.startSchedule.setFullYear(
487 currentDate.getFullYear(),
488 currentDate.getMonth(),
489 currentDate.getDate()
490 );
491 // Check if the start of the schedule is yesterday
492 if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
493 chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
494 }
495 } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
496 return null;
497 }
498 // Check if the charging profile is active
499 if (
500 moment(chargingSchedule.startSchedule)
501 .add(chargingSchedule.duration, 's')
502 .isAfter(currentMoment)
503 ) {
504 let lastButOneSchedule: ChargingSchedulePeriod;
505 // Search the right schedule period
506 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
507 // Handling of only one period
508 if (
509 chargingSchedule.chargingSchedulePeriod.length === 1 &&
510 schedulePeriod.startPeriod === 0
511 ) {
512 const result = {
513 limit: schedulePeriod.limit,
514 matchingChargingProfile: chargingProfile,
515 };
91a4f151 516 logger.debug(debugLogMsg, result);
17ac262c
JB
517 return result;
518 }
519 // Find the right schedule period
520 if (
521 moment(chargingSchedule.startSchedule)
522 .add(schedulePeriod.startPeriod, 's')
523 .isAfter(currentMoment)
524 ) {
525 // Found the schedule: last but one is the correct one
526 const result = {
527 limit: lastButOneSchedule.limit,
528 matchingChargingProfile: chargingProfile,
529 };
91a4f151 530 logger.debug(debugLogMsg, result);
17ac262c
JB
531 return result;
532 }
533 // Keep it
534 lastButOneSchedule = schedulePeriod;
535 // Handle the last schedule period
536 if (
537 schedulePeriod.startPeriod ===
538 chargingSchedule.chargingSchedulePeriod[
539 chargingSchedule.chargingSchedulePeriod.length - 1
540 ].startPeriod
541 ) {
542 const result = {
543 limit: lastButOneSchedule.limit,
544 matchingChargingProfile: chargingProfile,
545 };
91a4f151 546 logger.debug(debugLogMsg, result);
17ac262c
JB
547 return result;
548 }
549 }
550 }
551 }
552 return null;
553 }
554
555 private static getRandomSerialNumberSuffix(params?: {
556 randomBytesLength?: number;
557 upperCase?: boolean;
558 }): string {
559 const randomSerialNumberSuffix = crypto
560 .randomBytes(params?.randomBytesLength ?? 16)
561 .toString('hex');
562 if (params?.upperCase) {
563 return randomSerialNumberSuffix.toUpperCase();
564 }
565 return randomSerialNumberSuffix;
566 }
567}