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