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