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