refactor: cleanup null checks and helpers
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStationUtils.ts
1 import { createHash, randomBytes } from 'node:crypto';
2 import type { EventEmitter } from 'node:events';
3 import { basename, dirname, join } from 'node:path';
4 import { fileURLToPath } from 'node:url';
5
6 import chalk from 'chalk';
7 import moment from 'moment';
8
9 import type { ChargingStation } from './ChargingStation';
10 import { BaseError } from '../exception';
11 import {
12 AmpereUnits,
13 AvailabilityType,
14 type BootNotificationRequest,
15 BootReasonEnumType,
16 type ChargingProfile,
17 ChargingProfileKindType,
18 ChargingRateUnitType,
19 type ChargingSchedulePeriod,
20 type ChargingStationInfo,
21 type ChargingStationTemplate,
22 ChargingStationWorkerMessageEvents,
23 ConnectorPhaseRotation,
24 type ConnectorStatus,
25 ConnectorStatusEnum,
26 CurrentType,
27 type EvseTemplate,
28 type OCPP16BootNotificationRequest,
29 type OCPP20BootNotificationRequest,
30 OCPPVersion,
31 RecurrencyKindType,
32 Voltage,
33 } from '../types';
34 import {
35 ACElectricUtils,
36 Constants,
37 DCElectricUtils,
38 cloneObject,
39 convertToInt,
40 isEmptyObject,
41 isEmptyString,
42 isNotEmptyArray,
43 isNotEmptyString,
44 isNullOrUndefined,
45 isUndefined,
46 logger,
47 secureRandom,
48 } from '../utils';
49
50 const moduleName = 'ChargingStationUtils';
51
52 export const getChargingStationId = (
53 index: number,
54 stationTemplate: ChargingStationTemplate,
55 ): string => {
56 // In case of multiple instances: add instance index to charging station id
57 const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
58 const idSuffix = stationTemplate?.nameSuffix ?? '';
59 const idStr = `000000000${index.toString()}`;
60 return stationTemplate?.fixedName
61 ? stationTemplate.baseName
62 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
63 idStr.length - 4,
64 )}${idSuffix}`;
65 };
66
67 export const countReservableConnectors = (connectors: Map<number, ConnectorStatus>) => {
68 let reservableConnectors = 0;
69 for (const [connectorId, connectorStatus] of connectors) {
70 if (connectorId === 0) {
71 continue;
72 }
73 if (connectorStatus.status === ConnectorStatusEnum.Available) {
74 ++reservableConnectors;
75 }
76 }
77 return reservableConnectors;
78 };
79
80 export const getHashId = (index: number, stationTemplate: ChargingStationTemplate): string => {
81 const chargingStationInfo = {
82 chargePointModel: stationTemplate.chargePointModel,
83 chargePointVendor: stationTemplate.chargePointVendor,
84 ...(!isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && {
85 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix,
86 }),
87 ...(!isUndefined(stationTemplate.chargePointSerialNumberPrefix) && {
88 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix,
89 }),
90 ...(!isUndefined(stationTemplate.meterSerialNumberPrefix) && {
91 meterSerialNumber: stationTemplate.meterSerialNumberPrefix,
92 }),
93 ...(!isUndefined(stationTemplate.meterType) && {
94 meterType: stationTemplate.meterType,
95 }),
96 };
97 return createHash(Constants.DEFAULT_HASH_ALGORITHM)
98 .update(`${JSON.stringify(chargingStationInfo)}${getChargingStationId(index, stationTemplate)}`)
99 .digest('hex');
100 };
101
102 export const checkChargingStation = (
103 chargingStation: ChargingStation,
104 logPrefix: string,
105 ): boolean => {
106 if (chargingStation.started === false && chargingStation.starting === false) {
107 logger.warn(`${logPrefix} charging station is stopped, cannot proceed`);
108 return false;
109 }
110 return true;
111 };
112
113 export const getPhaseRotationValue = (
114 connectorId: number,
115 numberOfPhases: number,
116 ): string | undefined => {
117 // AC/DC
118 if (connectorId === 0 && numberOfPhases === 0) {
119 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
120 } else if (connectorId > 0 && numberOfPhases === 0) {
121 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
122 // AC
123 } else if (connectorId > 0 && numberOfPhases === 1) {
124 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
125 } else if (connectorId > 0 && numberOfPhases === 3) {
126 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
127 }
128 };
129
130 export const getMaxNumberOfEvses = (evses: Record<string, EvseTemplate>): number => {
131 if (!evses) {
132 return -1;
133 }
134 return Object.keys(evses).length;
135 };
136
137 const getMaxNumberOfConnectors = (connectors: Record<string, ConnectorStatus>): number => {
138 if (!connectors) {
139 return -1;
140 }
141 return Object.keys(connectors).length;
142 };
143
144 export const getBootConnectorStatus = (
145 chargingStation: ChargingStation,
146 connectorId: number,
147 connectorStatus: ConnectorStatus,
148 ): ConnectorStatusEnum => {
149 let connectorBootStatus: ConnectorStatusEnum;
150 if (
151 !connectorStatus?.status &&
152 (chargingStation.isChargingStationAvailable() === false ||
153 chargingStation.isConnectorAvailable(connectorId) === false)
154 ) {
155 connectorBootStatus = ConnectorStatusEnum.Unavailable;
156 } else if (!connectorStatus?.status && connectorStatus?.bootStatus) {
157 // Set boot status in template at startup
158 connectorBootStatus = connectorStatus?.bootStatus;
159 } else if (connectorStatus?.status) {
160 // Set previous status at startup
161 connectorBootStatus = connectorStatus?.status;
162 } else {
163 // Set default status
164 connectorBootStatus = ConnectorStatusEnum.Available;
165 }
166 return connectorBootStatus;
167 };
168
169 export const checkTemplate = (
170 stationTemplate: ChargingStationTemplate,
171 logPrefix: string,
172 templateFile: string,
173 ): void => {
174 if (isNullOrUndefined(stationTemplate)) {
175 const errorMsg = `Failed to read charging station template file ${templateFile}`;
176 logger.error(`${logPrefix} ${errorMsg}`);
177 throw new BaseError(errorMsg);
178 }
179 if (isEmptyObject(stationTemplate)) {
180 const errorMsg = `Empty charging station information from template file ${templateFile}`;
181 logger.error(`${logPrefix} ${errorMsg}`);
182 throw new BaseError(errorMsg);
183 }
184 if (isEmptyObject(stationTemplate.AutomaticTransactionGenerator!)) {
185 stationTemplate.AutomaticTransactionGenerator = Constants.DEFAULT_ATG_CONFIGURATION;
186 logger.warn(
187 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
188 Constants.DEFAULT_ATG_CONFIGURATION,
189 );
190 }
191 if (isNullOrUndefined(stationTemplate.idTagsFile) || isEmptyString(stationTemplate.idTagsFile)) {
192 logger.warn(
193 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`,
194 );
195 }
196 };
197
198 export const checkConnectorsConfiguration = (
199 stationTemplate: ChargingStationTemplate,
200 logPrefix: string,
201 templateFile: string,
202 ): {
203 configuredMaxConnectors: number;
204 templateMaxConnectors: number;
205 templateMaxAvailableConnectors: number;
206 } => {
207 const configuredMaxConnectors = getConfiguredNumberOfConnectors(stationTemplate);
208 checkConfiguredMaxConnectors(configuredMaxConnectors, logPrefix, templateFile);
209 const templateMaxConnectors = getMaxNumberOfConnectors(stationTemplate.Connectors!);
210 checkTemplateMaxConnectors(templateMaxConnectors, logPrefix, templateFile);
211 const templateMaxAvailableConnectors = stationTemplate.Connectors![0]
212 ? templateMaxConnectors - 1
213 : templateMaxConnectors;
214 if (
215 configuredMaxConnectors > templateMaxAvailableConnectors &&
216 !stationTemplate?.randomConnectors
217 ) {
218 logger.warn(
219 `${logPrefix} Number of connectors exceeds the number of connector configurations in template ${templateFile}, forcing random connector configurations affectation`,
220 );
221 stationTemplate.randomConnectors = true;
222 }
223 return { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors };
224 };
225
226 export const checkStationInfoConnectorStatus = (
227 connectorId: number,
228 connectorStatus: ConnectorStatus,
229 logPrefix: string,
230 templateFile: string,
231 ): void => {
232 if (!isNullOrUndefined(connectorStatus?.status)) {
233 logger.warn(
234 `${logPrefix} Charging station information from template ${templateFile} with connector id ${connectorId} status configuration defined, undefine it`,
235 );
236 delete connectorStatus.status;
237 }
238 };
239
240 export const buildConnectorsMap = (
241 connectors: Record<string, ConnectorStatus>,
242 logPrefix: string,
243 templateFile: string,
244 ): Map<number, ConnectorStatus> => {
245 const connectorsMap = new Map<number, ConnectorStatus>();
246 if (getMaxNumberOfConnectors(connectors) > 0) {
247 for (const connector in connectors) {
248 const connectorStatus = connectors[connector];
249 const connectorId = convertToInt(connector);
250 checkStationInfoConnectorStatus(connectorId, connectorStatus, logPrefix, templateFile);
251 connectorsMap.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus));
252 }
253 } else {
254 logger.warn(
255 `${logPrefix} Charging station information from template ${templateFile} with no connectors, cannot build connectors map`,
256 );
257 }
258 return connectorsMap;
259 };
260
261 export const initializeConnectorsMapStatus = (
262 connectors: Map<number, ConnectorStatus>,
263 logPrefix: string,
264 ): void => {
265 for (const connectorId of connectors.keys()) {
266 if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
267 logger.warn(
268 `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
269 connectorId,
270 )?.transactionId}`,
271 );
272 }
273 if (connectorId === 0) {
274 connectors.get(connectorId)!.availability = AvailabilityType.Operative;
275 if (isUndefined(connectors.get(connectorId)?.chargingProfiles)) {
276 connectors.get(connectorId)!.chargingProfiles = [];
277 }
278 } else if (
279 connectorId > 0 &&
280 isNullOrUndefined(connectors.get(connectorId)?.transactionStarted)
281 ) {
282 initializeConnectorStatus(connectors.get(connectorId)!);
283 }
284 }
285 };
286
287 export const resetConnectorStatus = (connectorStatus: ConnectorStatus): void => {
288 connectorStatus.idTagLocalAuthorized = false;
289 connectorStatus.idTagAuthorized = false;
290 connectorStatus.transactionRemoteStarted = false;
291 connectorStatus.transactionStarted = false;
292 delete connectorStatus?.localAuthorizeIdTag;
293 delete connectorStatus?.authorizeIdTag;
294 delete connectorStatus?.transactionId;
295 delete connectorStatus?.transactionIdTag;
296 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
297 delete connectorStatus?.transactionBeginMeterValue;
298 };
299
300 export const createBootNotificationRequest = (
301 stationInfo: ChargingStationInfo,
302 bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp,
303 ): BootNotificationRequest => {
304 const ocppVersion = stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
305 switch (ocppVersion) {
306 case OCPPVersion.VERSION_16:
307 return {
308 chargePointModel: stationInfo.chargePointModel,
309 chargePointVendor: stationInfo.chargePointVendor,
310 ...(!isUndefined(stationInfo.chargeBoxSerialNumber) && {
311 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber,
312 }),
313 ...(!isUndefined(stationInfo.chargePointSerialNumber) && {
314 chargePointSerialNumber: stationInfo.chargePointSerialNumber,
315 }),
316 ...(!isUndefined(stationInfo.firmwareVersion) && {
317 firmwareVersion: stationInfo.firmwareVersion,
318 }),
319 ...(!isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
320 ...(!isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
321 ...(!isUndefined(stationInfo.meterSerialNumber) && {
322 meterSerialNumber: stationInfo.meterSerialNumber,
323 }),
324 ...(!isUndefined(stationInfo.meterType) && {
325 meterType: stationInfo.meterType,
326 }),
327 } as OCPP16BootNotificationRequest;
328 case OCPPVersion.VERSION_20:
329 case OCPPVersion.VERSION_201:
330 return {
331 reason: bootReason,
332 chargingStation: {
333 model: stationInfo.chargePointModel,
334 vendorName: stationInfo.chargePointVendor,
335 ...(!isUndefined(stationInfo.firmwareVersion) && {
336 firmwareVersion: stationInfo.firmwareVersion,
337 }),
338 ...(!isUndefined(stationInfo.chargeBoxSerialNumber) && {
339 serialNumber: stationInfo.chargeBoxSerialNumber,
340 }),
341 ...((!isUndefined(stationInfo.iccid) || !isUndefined(stationInfo.imsi)) && {
342 modem: {
343 ...(!isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
344 ...(!isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
345 },
346 }),
347 },
348 } as OCPP20BootNotificationRequest;
349 }
350 };
351
352 export const warnTemplateKeysDeprecation = (
353 stationTemplate: ChargingStationTemplate,
354 logPrefix: string,
355 templateFile: string,
356 ) => {
357 const templateKeys: { deprecatedKey: string; key?: string }[] = [
358 { deprecatedKey: 'supervisionUrl', key: 'supervisionUrls' },
359 { deprecatedKey: 'authorizationFile', key: 'idTagsFile' },
360 { deprecatedKey: 'payloadSchemaValidation', key: 'ocppStrictCompliance' },
361 ];
362 for (const templateKey of templateKeys) {
363 warnDeprecatedTemplateKey(
364 stationTemplate,
365 templateKey.deprecatedKey,
366 logPrefix,
367 templateFile,
368 !isUndefined(templateKey.key) ? `Use '${templateKey.key}' instead` : undefined,
369 );
370 convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key);
371 }
372 };
373
374 export const stationTemplateToStationInfo = (
375 stationTemplate: ChargingStationTemplate,
376 ): ChargingStationInfo => {
377 stationTemplate = cloneObject<ChargingStationTemplate>(stationTemplate);
378 delete stationTemplate.power;
379 delete stationTemplate.powerUnit;
380 delete stationTemplate.Connectors;
381 delete stationTemplate.Evses;
382 delete stationTemplate.Configuration;
383 delete stationTemplate.AutomaticTransactionGenerator;
384 delete stationTemplate.chargeBoxSerialNumberPrefix;
385 delete stationTemplate.chargePointSerialNumberPrefix;
386 delete stationTemplate.meterSerialNumberPrefix;
387 return stationTemplate as unknown as ChargingStationInfo;
388 };
389
390 export const createSerialNumber = (
391 stationTemplate: ChargingStationTemplate,
392 stationInfo: ChargingStationInfo,
393 params: {
394 randomSerialNumberUpperCase?: boolean;
395 randomSerialNumber?: boolean;
396 } = {
397 randomSerialNumberUpperCase: true,
398 randomSerialNumber: true,
399 },
400 ): void => {
401 params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params };
402 const serialNumberSuffix = params?.randomSerialNumber
403 ? getRandomSerialNumberSuffix({
404 upperCase: params.randomSerialNumberUpperCase,
405 })
406 : '';
407 isNotEmptyString(stationTemplate?.chargePointSerialNumberPrefix) &&
408 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
409 isNotEmptyString(stationTemplate?.chargeBoxSerialNumberPrefix) &&
410 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
411 isNotEmptyString(stationTemplate?.meterSerialNumberPrefix) &&
412 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
413 };
414
415 export const propagateSerialNumber = (
416 stationTemplate: ChargingStationTemplate,
417 stationInfoSrc: ChargingStationInfo,
418 stationInfoDst: ChargingStationInfo,
419 ) => {
420 if (!stationInfoSrc || !stationTemplate) {
421 throw new BaseError(
422 'Missing charging station template or existing configuration to propagate serial number',
423 );
424 }
425 stationTemplate?.chargePointSerialNumberPrefix && stationInfoSrc?.chargePointSerialNumber
426 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
427 : stationInfoDst?.chargePointSerialNumber && delete stationInfoDst.chargePointSerialNumber;
428 stationTemplate?.chargeBoxSerialNumberPrefix && stationInfoSrc?.chargeBoxSerialNumber
429 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
430 : stationInfoDst?.chargeBoxSerialNumber && delete stationInfoDst.chargeBoxSerialNumber;
431 stationTemplate?.meterSerialNumberPrefix && stationInfoSrc?.meterSerialNumber
432 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
433 : stationInfoDst?.meterSerialNumber && delete stationInfoDst.meterSerialNumber;
434 };
435
436 export const getAmperageLimitationUnitDivider = (stationInfo: ChargingStationInfo): number => {
437 let unitDivider = 1;
438 switch (stationInfo.amperageLimitationUnit) {
439 case AmpereUnits.DECI_AMPERE:
440 unitDivider = 10;
441 break;
442 case AmpereUnits.CENTI_AMPERE:
443 unitDivider = 100;
444 break;
445 case AmpereUnits.MILLI_AMPERE:
446 unitDivider = 1000;
447 break;
448 }
449 return unitDivider;
450 };
451
452 export const getChargingStationConnectorChargingProfilesPowerLimit = (
453 chargingStation: ChargingStation,
454 connectorId: number,
455 ): number | undefined => {
456 let limit: number | undefined, matchingChargingProfile: ChargingProfile | undefined;
457 // Get charging profiles for connector and sort by stack level
458 const chargingProfiles =
459 cloneObject<ChargingProfile[]>(
460 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles!,
461 )?.sort((a, b) => b.stackLevel - a.stackLevel) ?? [];
462 // Get profiles on connector 0
463 if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
464 chargingProfiles.push(
465 ...cloneObject<ChargingProfile[]>(
466 chargingStation.getConnectorStatus(0)!.chargingProfiles!,
467 ).sort((a, b) => b.stackLevel - a.stackLevel),
468 );
469 }
470 if (isNotEmptyArray(chargingProfiles)) {
471 const result = getLimitFromChargingProfiles(chargingProfiles, chargingStation.logPrefix());
472 if (!isNullOrUndefined(result)) {
473 limit = result?.limit;
474 matchingChargingProfile = result?.matchingChargingProfile;
475 switch (chargingStation.getCurrentOutType()) {
476 case CurrentType.AC:
477 limit =
478 matchingChargingProfile?.chargingSchedule?.chargingRateUnit ===
479 ChargingRateUnitType.WATT
480 ? limit
481 : ACElectricUtils.powerTotal(
482 chargingStation.getNumberOfPhases(),
483 chargingStation.getVoltageOut(),
484 limit!,
485 );
486 break;
487 case CurrentType.DC:
488 limit =
489 matchingChargingProfile?.chargingSchedule?.chargingRateUnit ===
490 ChargingRateUnitType.WATT
491 ? limit
492 : DCElectricUtils.power(chargingStation.getVoltageOut(), limit!);
493 }
494 const connectorMaximumPower =
495 chargingStation.getMaximumPower() / chargingStation.powerDivider;
496 if (limit! > connectorMaximumPower) {
497 logger.error(
498 `${chargingStation.logPrefix()} Charging profile id ${matchingChargingProfile?.chargingProfileId} limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
499 result,
500 );
501 limit = connectorMaximumPower;
502 }
503 }
504 }
505 return limit;
506 };
507
508 export const getDefaultVoltageOut = (
509 currentType: CurrentType,
510 logPrefix: string,
511 templateFile: string,
512 ): Voltage => {
513 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
514 let defaultVoltageOut: number;
515 switch (currentType) {
516 case CurrentType.AC:
517 defaultVoltageOut = Voltage.VOLTAGE_230;
518 break;
519 case CurrentType.DC:
520 defaultVoltageOut = Voltage.VOLTAGE_400;
521 break;
522 default:
523 logger.error(`${logPrefix} ${errorMsg}`);
524 throw new BaseError(errorMsg);
525 }
526 return defaultVoltageOut;
527 };
528
529 export const getIdTagsFile = (stationInfo: ChargingStationInfo): string | undefined => {
530 return (
531 stationInfo.idTagsFile &&
532 join(dirname(fileURLToPath(import.meta.url)), 'assets', basename(stationInfo.idTagsFile))
533 );
534 };
535
536 export const waitChargingStationEvents = async (
537 emitter: EventEmitter,
538 event: ChargingStationWorkerMessageEvents,
539 eventsToWait: number,
540 ): Promise<number> => {
541 return new Promise((resolve) => {
542 let events = 0;
543 if (eventsToWait === 0) {
544 resolve(events);
545 }
546 emitter.on(event, () => {
547 ++events;
548 if (events === eventsToWait) {
549 resolve(events);
550 }
551 });
552 });
553 };
554
555 const getConfiguredNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
556 let configuredMaxConnectors = 0;
557 if (isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
558 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
559 configuredMaxConnectors =
560 numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)];
561 } else if (isUndefined(stationTemplate.numberOfConnectors) === false) {
562 configuredMaxConnectors = stationTemplate.numberOfConnectors as number;
563 } else if (stationTemplate.Connectors && !stationTemplate.Evses) {
564 configuredMaxConnectors = stationTemplate.Connectors[0]
565 ? getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
566 : getMaxNumberOfConnectors(stationTemplate.Connectors);
567 } else if (stationTemplate.Evses && !stationTemplate.Connectors) {
568 for (const evse in stationTemplate.Evses) {
569 if (evse === '0') {
570 continue;
571 }
572 configuredMaxConnectors += getMaxNumberOfConnectors(stationTemplate.Evses[evse].Connectors);
573 }
574 }
575 return configuredMaxConnectors;
576 };
577
578 const checkConfiguredMaxConnectors = (
579 configuredMaxConnectors: number,
580 logPrefix: string,
581 templateFile: string,
582 ): void => {
583 if (configuredMaxConnectors <= 0) {
584 logger.warn(
585 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
586 );
587 }
588 };
589
590 const checkTemplateMaxConnectors = (
591 templateMaxConnectors: number,
592 logPrefix: string,
593 templateFile: string,
594 ): void => {
595 if (templateMaxConnectors === 0) {
596 logger.warn(
597 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
598 );
599 } else if (templateMaxConnectors < 0) {
600 logger.error(
601 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
602 );
603 }
604 };
605
606 const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
607 connectorStatus.availability = AvailabilityType.Operative;
608 connectorStatus.idTagLocalAuthorized = false;
609 connectorStatus.idTagAuthorized = false;
610 connectorStatus.transactionRemoteStarted = false;
611 connectorStatus.transactionStarted = false;
612 connectorStatus.energyActiveImportRegisterValue = 0;
613 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
614 if (isUndefined(connectorStatus.chargingProfiles)) {
615 connectorStatus.chargingProfiles = [];
616 }
617 };
618
619 const warnDeprecatedTemplateKey = (
620 template: ChargingStationTemplate,
621 key: string,
622 logPrefix: string,
623 templateFile: string,
624 logMsgToAppend = '',
625 ): void => {
626 if (!isUndefined(template[key])) {
627 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
628 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
629 }`;
630 logger.warn(`${logPrefix} ${logMsg}`);
631 console.warn(chalk.yellow(`${logMsg}`));
632 }
633 };
634
635 const convertDeprecatedTemplateKey = (
636 template: ChargingStationTemplate,
637 deprecatedKey: string,
638 key?: string,
639 ): void => {
640 if (!isUndefined(template[deprecatedKey])) {
641 if (!isUndefined(key)) {
642 template[key!] = template[deprecatedKey] as unknown;
643 }
644 delete template[deprecatedKey];
645 }
646 };
647
648 /**
649 * Charging profiles should already be sorted by connector id and stack level (highest stack level has priority)
650 *
651 * @param chargingProfiles -
652 * @param logPrefix -
653 * @returns
654 */
655 const getLimitFromChargingProfiles = (
656 chargingProfiles: ChargingProfile[],
657 logPrefix: string,
658 ): {
659 limit: number;
660 matchingChargingProfile: ChargingProfile;
661 } | null => {
662 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
663 const currentMoment = moment();
664 const currentDate = new Date();
665 for (const chargingProfile of chargingProfiles) {
666 // Set helpers
667 const chargingSchedule = chargingProfile.chargingSchedule;
668 if (!chargingSchedule?.startSchedule) {
669 logger.warn(
670 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}`,
671 );
672 }
673 // Check type (recurring) and if it is already active
674 // Adjust the daily recurring schedule to today
675 if (
676 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
677 chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
678 currentMoment.isAfter(chargingSchedule.startSchedule)
679 ) {
680 if (!(chargingSchedule?.startSchedule instanceof Date)) {
681 logger.warn(
682 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object`,
683 );
684 chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule!);
685 }
686 chargingSchedule.startSchedule.setFullYear(
687 currentDate.getFullYear(),
688 currentDate.getMonth(),
689 currentDate.getDate(),
690 );
691 // Check if the start of the schedule is yesterday
692 if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
693 chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
694 }
695 } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
696 return null;
697 }
698 // Check if the charging profile is active
699 if (
700 moment(chargingSchedule.startSchedule)
701 .add(chargingSchedule.duration, 's')
702 .isAfter(currentMoment)
703 ) {
704 let lastButOneSchedule: ChargingSchedulePeriod | undefined;
705 // Search the right schedule period
706 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
707 // Handling of only one period
708 if (
709 chargingSchedule.chargingSchedulePeriod.length === 1 &&
710 schedulePeriod.startPeriod === 0
711 ) {
712 const result = {
713 limit: schedulePeriod.limit,
714 matchingChargingProfile: chargingProfile,
715 };
716 logger.debug(debugLogMsg, result);
717 return result;
718 }
719 // Find the right schedule period
720 if (
721 moment(chargingSchedule.startSchedule)
722 .add(schedulePeriod.startPeriod, 's')
723 .isAfter(currentMoment)
724 ) {
725 // Found the schedule: last but one is the correct one
726 const result = {
727 limit: lastButOneSchedule!.limit,
728 matchingChargingProfile: chargingProfile,
729 };
730 logger.debug(debugLogMsg, result);
731 return result;
732 }
733 // Keep it
734 lastButOneSchedule = schedulePeriod;
735 // Handle the last schedule period
736 if (
737 schedulePeriod.startPeriod ===
738 chargingSchedule.chargingSchedulePeriod[
739 chargingSchedule.chargingSchedulePeriod.length - 1
740 ].startPeriod
741 ) {
742 const result = {
743 limit: lastButOneSchedule.limit,
744 matchingChargingProfile: chargingProfile,
745 };
746 logger.debug(debugLogMsg, result);
747 return result;
748 }
749 }
750 }
751 }
752 return null;
753 };
754
755 const getRandomSerialNumberSuffix = (params?: {
756 randomBytesLength?: number;
757 upperCase?: boolean;
758 }): string => {
759 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex');
760 if (params?.upperCase) {
761 return randomSerialNumberSuffix.toUpperCase();
762 }
763 return randomSerialNumberSuffix;
764 };