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