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