1dffb0377949d4dc72dff8d84e2ec1235ff7161e
[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 differenceInDays,
12 differenceInWeeks,
13 endOfDay,
14 endOfWeek,
15 isAfter,
16 isBefore,
17 startOfDay,
18 startOfWeek,
19 } from 'date-fns';
20
21 import type { ChargingStation } from './ChargingStation';
22 import { BaseError } from '../exception';
23 import {
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';
46 import {
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
63 const moduleName = 'ChargingStationUtils';
64
65 export 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
80 export 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
93 export 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
115 export 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
126 export 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
143 export const getMaxNumberOfEvses = (evses: Record<string, EvseTemplate>): number => {
144 if (!evses) {
145 return -1;
146 }
147 return Object.keys(evses).length;
148 };
149
150 const getMaxNumberOfConnectors = (connectors: Record<string, ConnectorStatus>): number => {
151 if (!connectors) {
152 return -1;
153 }
154 return Object.keys(connectors).length;
155 };
156
157 export 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
182 export 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
211 export 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
239 export 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
253 export 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
274 export 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
300 export 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
313 export 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
365 export 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
387 export 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
403 export 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
428 export 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
449 export 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
465 export 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
521 export 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
542 export 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
549 export 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
568 const 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
591 const 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
603 const 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
619 const 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
632 const 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
648 const 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 */
669 const 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 start schedule
695 if (chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING) {
696 switch (chargingProfile.recurrencyKind) {
697 case RecurrencyKindType.DAILY:
698 if (isBefore(chargingSchedule.startSchedule, startOfDay(currentDate))) {
699 addDays(
700 chargingSchedule.startSchedule,
701 differenceInDays(chargingSchedule.startSchedule, endOfDay(currentDate)),
702 );
703 }
704 break;
705 case RecurrencyKindType.WEEKLY:
706 if (isBefore(chargingSchedule.startSchedule, startOfWeek(currentDate))) {
707 addWeeks(
708 chargingSchedule.startSchedule,
709 differenceInWeeks(chargingSchedule.startSchedule, endOfWeek(currentDate)),
710 );
711 }
712 break;
713 }
714 }
715 // Check if the charging profile is active
716 if (
717 isAfter(addSeconds(chargingSchedule.startSchedule, chargingSchedule.duration!), currentDate)
718 ) {
719 let lastButOneSchedule: ChargingSchedulePeriod | undefined;
720 // Search the right schedule period
721 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
722 // Handling of only one period
723 if (
724 chargingSchedule.chargingSchedulePeriod.length === 1 &&
725 schedulePeriod.startPeriod === 0
726 ) {
727 const result = {
728 limit: schedulePeriod.limit,
729 matchingChargingProfile: chargingProfile,
730 };
731 logger.debug(debugLogMsg, result);
732 return result;
733 }
734 // Find the right schedule period
735 if (
736 isAfter(
737 addSeconds(chargingSchedule.startSchedule, schedulePeriod.startPeriod),
738 currentDate,
739 )
740 ) {
741 // Found the schedule: last but one is the correct one
742 const result = {
743 limit: lastButOneSchedule!.limit,
744 matchingChargingProfile: chargingProfile,
745 };
746 logger.debug(debugLogMsg, result);
747 return result;
748 }
749 // Keep it
750 lastButOneSchedule = schedulePeriod;
751 // Handle the last schedule period
752 if (
753 schedulePeriod.startPeriod ===
754 chargingSchedule.chargingSchedulePeriod[
755 chargingSchedule.chargingSchedulePeriod.length - 1
756 ].startPeriod
757 ) {
758 const result = {
759 limit: lastButOneSchedule.limit,
760 matchingChargingProfile: chargingProfile,
761 };
762 logger.debug(debugLogMsg, result);
763 return result;
764 }
765 }
766 }
767 }
768 };
769
770 const getRandomSerialNumberSuffix = (params?: {
771 randomBytesLength?: number;
772 upperCase?: boolean;
773 }): string => {
774 const randomSerialNumberSuffix = randomBytes(params?.randomBytesLength ?? 16).toString('hex');
775 if (params?.upperCase) {
776 return randomSerialNumberSuffix.toUpperCase();
777 }
778 return randomSerialNumberSuffix;
779 };