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