build(deps-dev): apply updates
[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 configuredMaxConnectors = 0;
569 for (const evse in stationTemplate.Evses) {
570 if (evse === '0') {
571 continue;
572 }
573 configuredMaxConnectors += getMaxNumberOfConnectors(stationTemplate.Evses[evse].Connectors);
574 }
575 }
576 return configuredMaxConnectors;
577 };
578
579 const checkConfiguredMaxConnectors = (
580 configuredMaxConnectors: number,
581 logPrefix: string,
582 templateFile: string,
583 ): void => {
584 if (configuredMaxConnectors <= 0) {
585 logger.warn(
586 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`,
587 );
588 }
589 };
590
591 const checkTemplateMaxConnectors = (
592 templateMaxConnectors: number,
593 logPrefix: string,
594 templateFile: string,
595 ): void => {
596 if (templateMaxConnectors === 0) {
597 logger.warn(
598 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`,
599 );
600 } else if (templateMaxConnectors < 0) {
601 logger.error(
602 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`,
603 );
604 }
605 };
606
607 const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
608 connectorStatus.availability = AvailabilityType.Operative;
609 connectorStatus.idTagLocalAuthorized = false;
610 connectorStatus.idTagAuthorized = false;
611 connectorStatus.transactionRemoteStarted = false;
612 connectorStatus.transactionStarted = false;
613 connectorStatus.energyActiveImportRegisterValue = 0;
614 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
615 if (isUndefined(connectorStatus.chargingProfiles)) {
616 connectorStatus.chargingProfiles = [];
617 }
618 };
619
620 const warnDeprecatedTemplateKey = (
621 template: ChargingStationTemplate,
622 key: string,
623 logPrefix: string,
624 templateFile: string,
625 logMsgToAppend = '',
626 ): void => {
627 if (!isUndefined(template[key])) {
628 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
629 isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
630 }`;
631 logger.warn(`${logPrefix} ${logMsg}`);
632 console.warn(chalk.yellow(`${logMsg}`));
633 }
634 };
635
636 const convertDeprecatedTemplateKey = (
637 template: ChargingStationTemplate,
638 deprecatedKey: string,
639 key?: string,
640 ): void => {
641 if (!isUndefined(template[deprecatedKey])) {
642 if (!isUndefined(key)) {
643 template[key!] = template[deprecatedKey] as unknown;
644 }
645 delete template[deprecatedKey];
646 }
647 };
648
649 /**
650 * Charging profiles should already be sorted by connector id and stack level (highest stack level has priority)
651 *
652 * @param chargingProfiles -
653 * @param logPrefix -
654 * @returns
655 */
656 const getLimitFromChargingProfiles = (
657 chargingProfiles: ChargingProfile[],
658 logPrefix: string,
659 ): {
660 limit: number;
661 matchingChargingProfile: ChargingProfile;
662 } | null => {
663 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
664 const currentMoment = moment();
665 const currentDate = new Date();
666 for (const chargingProfile of chargingProfiles) {
667 // Set helpers
668 const chargingSchedule = chargingProfile.chargingSchedule;
669 if (!chargingSchedule?.startSchedule) {
670 logger.warn(
671 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}`,
672 );
673 }
674 // Check type (recurring) and if it is already active
675 // Adjust the daily recurring schedule to today
676 if (
677 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
678 chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
679 currentMoment.isAfter(chargingSchedule.startSchedule)
680 ) {
681 if (!(chargingSchedule?.startSchedule instanceof Date)) {
682 logger.warn(
683 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object`,
684 );
685 chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule!);
686 }
687 chargingSchedule.startSchedule.setFullYear(
688 currentDate.getFullYear(),
689 currentDate.getMonth(),
690 currentDate.getDate(),
691 );
692 // Check if the start of the schedule is yesterday
693 if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
694 chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
695 }
696 } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
697 return null;
698 }
699 // Check if the charging profile is active
700 if (
701 moment(chargingSchedule.startSchedule)
702 .add(chargingSchedule.duration, 's')
703 .isAfter(currentMoment)
704 ) {
705 let lastButOneSchedule: ChargingSchedulePeriod | undefined;
706 // Search the right schedule period
707 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
708 // Handling of only one period
709 if (
710 chargingSchedule.chargingSchedulePeriod.length === 1 &&
711 schedulePeriod.startPeriod === 0
712 ) {
713 const result = {
714 limit: schedulePeriod.limit,
715 matchingChargingProfile: chargingProfile,
716 };
717 logger.debug(debugLogMsg, result);
718 return result;
719 }
720 // Find the right schedule period
721 if (
722 moment(chargingSchedule.startSchedule)
723 .add(schedulePeriod.startPeriod, 's')
724 .isAfter(currentMoment)
725 ) {
726 // Found the schedule: last but one is the correct one
727 const result = {
728 limit: lastButOneSchedule!.limit,
729 matchingChargingProfile: chargingProfile,
730 };
731 logger.debug(debugLogMsg, result);
732 return result;
733 }
734 // Keep it
735 lastButOneSchedule = schedulePeriod;
736 // Handle the last schedule period
737 if (
738 schedulePeriod.startPeriod ===
739 chargingSchedule.chargingSchedulePeriod[
740 chargingSchedule.chargingSchedulePeriod.length - 1
741 ].startPeriod
742 ) {
743 const result = {
744 limit: lastButOneSchedule.limit,
745 matchingChargingProfile: chargingProfile,
746 };
747 logger.debug(debugLogMsg, result);
748 return result;
749 }
750 }
751 }
752 }
753 return null;
754 };
755
756 const getRandomSerialNumberSuffix = (params?: {
757 randomBytesLength?: number;
758 upperCase?: boolean;
759 }): string => {
760 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex');
761 if (params?.upperCase) {
762 return randomSerialNumberSuffix.toUpperCase();
763 }
764 return randomSerialNumberSuffix;
765 };