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