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