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