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