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