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