51ea7931a0f9d55e22beefea195d496c151af8bc
[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`,
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, matchingChargingProfile: ChargingProfile;
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 === ChargingRateUnitType.WATT
479 ? limit
480 : ACElectricUtils.powerTotal(
481 chargingStation.getNumberOfPhases(),
482 chargingStation.getVoltageOut(),
483 limit,
484 );
485 break;
486 case CurrentType.DC:
487 limit =
488 matchingChargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
489 ? limit
490 : DCElectricUtils.power(chargingStation.getVoltageOut(), limit);
491 }
492 const connectorMaximumPower =
493 chargingStation.getMaximumPower() / chargingStation.powerDivider;
494 if (limit > connectorMaximumPower) {
495 logger.error(
496 `${chargingStation.logPrefix()} Charging profile id ${
497 matchingChargingProfile.chargingProfileId
498 } 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: number;
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 template[key] = template[deprecatedKey] as unknown;
643 delete template[deprecatedKey];
644 }
645 };
646
647 /**
648 * Charging profiles should already be sorted by connector id and stack level (highest stack level has priority)
649 *
650 * @param chargingProfiles -
651 * @param logPrefix -
652 * @returns
653 */
654 const getLimitFromChargingProfiles = (
655 chargingProfiles: ChargingProfile[],
656 logPrefix: string,
657 ): {
658 limit: number;
659 matchingChargingProfile: ChargingProfile;
660 } | null => {
661 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
662 const currentMoment = moment();
663 const currentDate = new Date();
664 for (const chargingProfile of chargingProfiles) {
665 // Set helpers
666 const chargingSchedule = chargingProfile.chargingSchedule;
667 if (!chargingSchedule?.startSchedule) {
668 logger.warn(
669 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}`,
670 );
671 }
672 // Check type (recurring) and if it is already active
673 // Adjust the daily recurring schedule to today
674 if (
675 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
676 chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
677 currentMoment.isAfter(chargingSchedule.startSchedule)
678 ) {
679 if (!(chargingSchedule?.startSchedule instanceof Date)) {
680 logger.warn(
681 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object`,
682 );
683 chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule);
684 }
685 chargingSchedule.startSchedule.setFullYear(
686 currentDate.getFullYear(),
687 currentDate.getMonth(),
688 currentDate.getDate(),
689 );
690 // Check if the start of the schedule is yesterday
691 if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
692 chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
693 }
694 } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
695 return null;
696 }
697 // Check if the charging profile is active
698 if (
699 moment(chargingSchedule.startSchedule)
700 .add(chargingSchedule.duration, 's')
701 .isAfter(currentMoment)
702 ) {
703 let lastButOneSchedule: ChargingSchedulePeriod;
704 // Search the right schedule period
705 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
706 // Handling of only one period
707 if (
708 chargingSchedule.chargingSchedulePeriod.length === 1 &&
709 schedulePeriod.startPeriod === 0
710 ) {
711 const result = {
712 limit: schedulePeriod.limit,
713 matchingChargingProfile: chargingProfile,
714 };
715 logger.debug(debugLogMsg, result);
716 return result;
717 }
718 // Find the right schedule period
719 if (
720 moment(chargingSchedule.startSchedule)
721 .add(schedulePeriod.startPeriod, 's')
722 .isAfter(currentMoment)
723 ) {
724 // Found the schedule: last but one is the correct one
725 const result = {
726 limit: lastButOneSchedule.limit,
727 matchingChargingProfile: chargingProfile,
728 };
729 logger.debug(debugLogMsg, result);
730 return result;
731 }
732 // Keep it
733 lastButOneSchedule = schedulePeriod;
734 // Handle the last schedule period
735 if (
736 schedulePeriod.startPeriod ===
737 chargingSchedule.chargingSchedulePeriod[
738 chargingSchedule.chargingSchedulePeriod.length - 1
739 ].startPeriod
740 ) {
741 const result = {
742 limit: lastButOneSchedule.limit,
743 matchingChargingProfile: chargingProfile,
744 };
745 logger.debug(debugLogMsg, result);
746 return result;
747 }
748 }
749 }
750 }
751 return null;
752 };
753
754 const getRandomSerialNumberSuffix = (params?: {
755 randomBytesLength?: number;
756 upperCase?: boolean;
757 }): string => {
758 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex');
759 if (params?.upperCase) {
760 return randomSerialNumberSuffix.toUpperCase();
761 }
762 return randomSerialNumberSuffix;
763 };