refactor(simulator): rename checkTemplateFile() -> checkTemplate()
[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 { ACElectricUtils, Constants, DCElectricUtils, Utils, logger } from '../utils';
33
34 const moduleName = 'ChargingStationUtils';
35
36 export class ChargingStationUtils {
37 private constructor() {
38 // This is intentional
39 }
40
41 public static getChargingStationId(
42 index: number,
43 stationTemplate: ChargingStationTemplate
44 ): string {
45 // In case of multiple instances: add instance index to charging station id
46 const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
47 const idSuffix = stationTemplate?.nameSuffix ?? '';
48 const idStr = `000000000${index.toString()}`;
49 return stationTemplate?.fixedName
50 ? stationTemplate.baseName
51 : `${stationTemplate.baseName}-${instanceIndex.toString()}${idStr.substring(
52 idStr.length - 4
53 )}${idSuffix}`;
54 }
55
56 public static getHashId(index: number, stationTemplate: ChargingStationTemplate): string {
57 const chargingStationInfo = {
58 chargePointModel: stationTemplate.chargePointModel,
59 chargePointVendor: stationTemplate.chargePointVendor,
60 ...(!Utils.isUndefined(stationTemplate.chargeBoxSerialNumberPrefix) && {
61 chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix,
62 }),
63 ...(!Utils.isUndefined(stationTemplate.chargePointSerialNumberPrefix) && {
64 chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix,
65 }),
66 ...(!Utils.isUndefined(stationTemplate.meterSerialNumberPrefix) && {
67 meterSerialNumber: stationTemplate.meterSerialNumberPrefix,
68 }),
69 ...(!Utils.isUndefined(stationTemplate.meterType) && {
70 meterType: stationTemplate.meterType,
71 }),
72 };
73 return crypto
74 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
75 .update(
76 `${JSON.stringify(chargingStationInfo)}${ChargingStationUtils.getChargingStationId(
77 index,
78 stationTemplate
79 )}`
80 )
81 .digest('hex');
82 }
83
84 public static checkChargingStation(chargingStation: ChargingStation, logPrefix: string): boolean {
85 if (chargingStation.started === false && chargingStation.starting === false) {
86 logger.warn(`${logPrefix} charging station is stopped, cannot proceed`);
87 return false;
88 }
89 return true;
90 }
91
92 public static getPhaseRotationValue(
93 connectorId: number,
94 numberOfPhases: number
95 ): string | undefined {
96 // AC/DC
97 if (connectorId === 0 && numberOfPhases === 0) {
98 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
99 } else if (connectorId > 0 && numberOfPhases === 0) {
100 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
101 // AC
102 } else if (connectorId > 0 && numberOfPhases === 1) {
103 return `${connectorId}.${ConnectorPhaseRotation.NotApplicable}`;
104 } else if (connectorId > 0 && numberOfPhases === 3) {
105 return `${connectorId}.${ConnectorPhaseRotation.RST}`;
106 }
107 }
108
109 public static getMaxNumberOfEvses(evses: Record<string, EvseTemplate>): number {
110 if (!evses) {
111 return -1;
112 }
113 return Object.keys(evses).length;
114 }
115
116 public static getMaxNumberOfConnectors(connectors: Record<string, ConnectorStatus>): number {
117 if (!connectors) {
118 return -1;
119 }
120 return Object.keys(connectors).length;
121 }
122
123 public static getBootConnectorStatus(
124 chargingStation: ChargingStation,
125 connectorId: number,
126 connectorStatus: ConnectorStatus
127 ): ConnectorStatusEnum {
128 let connectorBootStatus: ConnectorStatusEnum;
129 if (
130 !connectorStatus?.status &&
131 (chargingStation.isChargingStationAvailable() === false ||
132 chargingStation.isConnectorAvailable(connectorId) === false)
133 ) {
134 connectorBootStatus = ConnectorStatusEnum.Unavailable;
135 } else if (!connectorStatus?.status && connectorStatus?.bootStatus) {
136 // Set boot status in template at startup
137 connectorBootStatus = connectorStatus?.bootStatus;
138 } else if (connectorStatus?.status) {
139 // Set previous status at startup
140 connectorBootStatus = connectorStatus?.status;
141 } else {
142 // Set default status
143 connectorBootStatus = ConnectorStatusEnum.Available;
144 }
145 return connectorBootStatus;
146 }
147
148 public static checkTemplate(
149 stationTemplate: ChargingStationTemplate,
150 logPrefix: string,
151 templateFile: string
152 ) {
153 if (Utils.isNullOrUndefined(stationTemplate)) {
154 const errorMsg = `Failed to read charging station template file ${templateFile}`;
155 logger.error(`${logPrefix} ${errorMsg}`);
156 throw new BaseError(errorMsg);
157 }
158 if (Utils.isEmptyObject(stationTemplate)) {
159 const errorMsg = `Empty charging station information from template file ${templateFile}`;
160 logger.error(`${logPrefix} ${errorMsg}`);
161 throw new BaseError(errorMsg);
162 }
163 if (Utils.isEmptyObject(stationTemplate.AutomaticTransactionGenerator)) {
164 stationTemplate.AutomaticTransactionGenerator = {
165 enable: false,
166 minDuration: 60,
167 maxDuration: 120,
168 minDelayBetweenTwoTransactions: 15,
169 maxDelayBetweenTwoTransactions: 30,
170 probabilityOfStart: 1,
171 stopAfterHours: 0.3,
172 stopOnConnectionFailure: true,
173 };
174 logger.warn(
175 `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default values`
176 );
177 }
178 if (
179 Utils.isNullOrUndefined(stationTemplate.idTagsFile) ||
180 Utils.isEmptyString(stationTemplate.idTagsFile)
181 ) {
182 logger.warn(
183 `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
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 warnTemplateKeysDeprecation(
355 stationTemplate: ChargingStationTemplate,
356 logPrefix: string,
357 templateFile: string
358 ) {
359 const templateKeys: { key: string; deprecatedKey: string }[] = [
360 { key: 'supervisionUrls', deprecatedKey: 'supervisionUrl' },
361 { key: 'idTagsFile', deprecatedKey: 'authorizationFile' },
362 ];
363 for (const templateKey of templateKeys) {
364 ChargingStationUtils.warnDeprecatedTemplateKey(
365 stationTemplate,
366 templateKey.deprecatedKey,
367 logPrefix,
368 templateFile,
369 `Use '${templateKey.key}' instead`
370 );
371 ChargingStationUtils.convertDeprecatedTemplateKey(
372 stationTemplate,
373 templateKey.deprecatedKey,
374 templateKey.key
375 );
376 }
377 }
378
379 public static stationTemplateToStationInfo(
380 stationTemplate: ChargingStationTemplate
381 ): ChargingStationInfo {
382 stationTemplate = Utils.cloneObject<ChargingStationTemplate>(stationTemplate);
383 delete stationTemplate.power;
384 delete stationTemplate.powerUnit;
385 delete stationTemplate?.Connectors;
386 delete stationTemplate?.Evses;
387 delete stationTemplate.Configuration;
388 delete stationTemplate.AutomaticTransactionGenerator;
389 delete stationTemplate.chargeBoxSerialNumberPrefix;
390 delete stationTemplate.chargePointSerialNumberPrefix;
391 delete stationTemplate.meterSerialNumberPrefix;
392 return stationTemplate as unknown as ChargingStationInfo;
393 }
394
395 public static createSerialNumber(
396 stationTemplate: ChargingStationTemplate,
397 stationInfo: ChargingStationInfo,
398 params: {
399 randomSerialNumberUpperCase?: boolean;
400 randomSerialNumber?: boolean;
401 } = {
402 randomSerialNumberUpperCase: true,
403 randomSerialNumber: true,
404 }
405 ): void {
406 params = { ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true }, ...params };
407 const serialNumberSuffix = params?.randomSerialNumber
408 ? ChargingStationUtils.getRandomSerialNumberSuffix({
409 upperCase: params.randomSerialNumberUpperCase,
410 })
411 : '';
412 Utils.isNotEmptyString(stationTemplate?.chargePointSerialNumberPrefix) &&
413 (stationInfo.chargePointSerialNumber = `${stationTemplate.chargePointSerialNumberPrefix}${serialNumberSuffix}`);
414 Utils.isNotEmptyString(stationTemplate?.chargeBoxSerialNumberPrefix) &&
415 (stationInfo.chargeBoxSerialNumber = `${stationTemplate.chargeBoxSerialNumberPrefix}${serialNumberSuffix}`);
416 Utils.isNotEmptyString(stationTemplate?.meterSerialNumberPrefix) &&
417 (stationInfo.meterSerialNumber = `${stationTemplate.meterSerialNumberPrefix}${serialNumberSuffix}`);
418 }
419
420 public static propagateSerialNumber(
421 stationTemplate: ChargingStationTemplate,
422 stationInfoSrc: ChargingStationInfo,
423 stationInfoDst: ChargingStationInfo
424 ) {
425 if (!stationInfoSrc || !stationTemplate) {
426 throw new BaseError(
427 'Missing charging station template or existing configuration to propagate serial number'
428 );
429 }
430 stationTemplate?.chargePointSerialNumberPrefix && stationInfoSrc?.chargePointSerialNumber
431 ? (stationInfoDst.chargePointSerialNumber = stationInfoSrc.chargePointSerialNumber)
432 : stationInfoDst?.chargePointSerialNumber && delete stationInfoDst.chargePointSerialNumber;
433 stationTemplate?.chargeBoxSerialNumberPrefix && stationInfoSrc?.chargeBoxSerialNumber
434 ? (stationInfoDst.chargeBoxSerialNumber = stationInfoSrc.chargeBoxSerialNumber)
435 : stationInfoDst?.chargeBoxSerialNumber && delete stationInfoDst.chargeBoxSerialNumber;
436 stationTemplate?.meterSerialNumberPrefix && stationInfoSrc?.meterSerialNumber
437 ? (stationInfoDst.meterSerialNumber = stationInfoSrc.meterSerialNumber)
438 : stationInfoDst?.meterSerialNumber && delete stationInfoDst.meterSerialNumber;
439 }
440
441 public static getAmperageLimitationUnitDivider(stationInfo: ChargingStationInfo): number {
442 let unitDivider = 1;
443 switch (stationInfo.amperageLimitationUnit) {
444 case AmpereUnits.DECI_AMPERE:
445 unitDivider = 10;
446 break;
447 case AmpereUnits.CENTI_AMPERE:
448 unitDivider = 100;
449 break;
450 case AmpereUnits.MILLI_AMPERE:
451 unitDivider = 1000;
452 break;
453 }
454 return unitDivider;
455 }
456
457 public static getChargingStationConnectorChargingProfilesPowerLimit(
458 chargingStation: ChargingStation,
459 connectorId: number
460 ): number | undefined {
461 let limit: number, matchingChargingProfile: ChargingProfile;
462 // Get charging profiles for connector and sort by stack level
463 const chargingProfiles =
464 Utils.cloneObject<ChargingProfile[]>(
465 chargingStation.getConnectorStatus(connectorId)?.chargingProfiles
466 )?.sort((a, b) => b.stackLevel - a.stackLevel) ?? [];
467 // Get profiles on connector 0
468 if (chargingStation.getConnectorStatus(0)?.chargingProfiles) {
469 chargingProfiles.push(
470 ...Utils.cloneObject<ChargingProfile[]>(
471 chargingStation.getConnectorStatus(0).chargingProfiles
472 ).sort((a, b) => b.stackLevel - a.stackLevel)
473 );
474 }
475 if (Utils.isNotEmptyArray(chargingProfiles)) {
476 const result = ChargingStationUtils.getLimitFromChargingProfiles(
477 chargingProfiles,
478 chargingStation.logPrefix()
479 );
480 if (!Utils.isNullOrUndefined(result)) {
481 limit = result?.limit;
482 matchingChargingProfile = result?.matchingChargingProfile;
483 switch (chargingStation.getCurrentOutType()) {
484 case CurrentType.AC:
485 limit =
486 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
487 ChargingRateUnitType.WATT
488 ? limit
489 : ACElectricUtils.powerTotal(
490 chargingStation.getNumberOfPhases(),
491 chargingStation.getVoltageOut(),
492 limit
493 );
494 break;
495 case CurrentType.DC:
496 limit =
497 matchingChargingProfile.chargingSchedule.chargingRateUnit ===
498 ChargingRateUnitType.WATT
499 ? limit
500 : DCElectricUtils.power(chargingStation.getVoltageOut(), limit);
501 }
502 const connectorMaximumPower =
503 chargingStation.getMaximumPower() / chargingStation.powerDivider;
504 if (limit > connectorMaximumPower) {
505 logger.error(
506 `${chargingStation.logPrefix()} Charging profile id ${
507 matchingChargingProfile.chargingProfileId
508 } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}: %j`,
509 result
510 );
511 limit = connectorMaximumPower;
512 }
513 }
514 }
515 return limit;
516 }
517
518 public static getDefaultVoltageOut(
519 currentType: CurrentType,
520 logPrefix: string,
521 templateFile: string
522 ): Voltage {
523 const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`;
524 let defaultVoltageOut: number;
525 switch (currentType) {
526 case CurrentType.AC:
527 defaultVoltageOut = Voltage.VOLTAGE_230;
528 break;
529 case CurrentType.DC:
530 defaultVoltageOut = Voltage.VOLTAGE_400;
531 break;
532 default:
533 logger.error(`${logPrefix} ${errorMsg}`);
534 throw new BaseError(errorMsg);
535 }
536 return defaultVoltageOut;
537 }
538
539 public static getIdTagsFile(stationInfo: ChargingStationInfo): string | undefined {
540 return (
541 stationInfo.idTagsFile &&
542 path.join(
543 path.dirname(fileURLToPath(import.meta.url)),
544 'assets',
545 path.basename(stationInfo.idTagsFile)
546 )
547 );
548 }
549
550 private static getConfiguredNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
551 let configuredMaxConnectors: number;
552 if (Utils.isNotEmptyArray(stationTemplate.numberOfConnectors) === true) {
553 const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
554 configuredMaxConnectors =
555 numberOfConnectors[Math.floor(Utils.secureRandom() * numberOfConnectors.length)];
556 } else if (Utils.isUndefined(stationTemplate.numberOfConnectors) === false) {
557 configuredMaxConnectors = stationTemplate.numberOfConnectors as number;
558 } else if (stationTemplate.Connectors && !stationTemplate.Evses) {
559 configuredMaxConnectors = stationTemplate?.Connectors[0]
560 ? ChargingStationUtils.getMaxNumberOfConnectors(stationTemplate.Connectors) - 1
561 : ChargingStationUtils.getMaxNumberOfConnectors(stationTemplate.Connectors);
562 } else if (stationTemplate.Evses && !stationTemplate.Connectors) {
563 configuredMaxConnectors = 0;
564 for (const evse in stationTemplate.Evses) {
565 if (evse === '0') {
566 continue;
567 }
568 configuredMaxConnectors += ChargingStationUtils.getMaxNumberOfConnectors(
569 stationTemplate.Evses[evse].Connectors
570 );
571 }
572 }
573 return configuredMaxConnectors;
574 }
575
576 private static checkConfiguredMaxConnectors(
577 configuredMaxConnectors: number,
578 logPrefix: string,
579 templateFile: string
580 ): void {
581 if (configuredMaxConnectors <= 0) {
582 logger.warn(
583 `${logPrefix} Charging station information from template ${templateFile} with ${configuredMaxConnectors} connectors`
584 );
585 }
586 }
587
588 private static checkTemplateMaxConnectors(
589 templateMaxConnectors: number,
590 logPrefix: string,
591 templateFile: string
592 ): void {
593 if (templateMaxConnectors === 0) {
594 logger.warn(
595 `${logPrefix} Charging station information from template ${templateFile} with empty connectors configuration`
596 );
597 } else if (templateMaxConnectors < 0) {
598 logger.error(
599 `${logPrefix} Charging station information from template ${templateFile} with no connectors configuration defined`
600 );
601 }
602 }
603
604 private static initializeConnectorStatus(connectorStatus: ConnectorStatus): void {
605 connectorStatus.availability = AvailabilityType.Operative;
606 connectorStatus.idTagLocalAuthorized = false;
607 connectorStatus.idTagAuthorized = false;
608 connectorStatus.transactionRemoteStarted = false;
609 connectorStatus.transactionStarted = false;
610 connectorStatus.energyActiveImportRegisterValue = 0;
611 connectorStatus.transactionEnergyActiveImportRegisterValue = 0;
612 if (Utils.isUndefined(connectorStatus.chargingProfiles)) {
613 connectorStatus.chargingProfiles = [];
614 }
615 }
616
617 private static warnDeprecatedTemplateKey(
618 template: ChargingStationTemplate,
619 key: string,
620 logPrefix: string,
621 templateFile: string,
622 logMsgToAppend = ''
623 ): void {
624 if (!Utils.isUndefined(template[key])) {
625 const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
626 Utils.isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
627 }`;
628 logger.warn(`${logPrefix} ${logMsg}`);
629 console.warn(chalk.yellow(`${logMsg}`));
630 }
631 }
632
633 private static convertDeprecatedTemplateKey(
634 template: ChargingStationTemplate,
635 deprecatedKey: string,
636 key: string
637 ): void {
638 if (!Utils.isUndefined(template[deprecatedKey])) {
639 template[key] = template[deprecatedKey] as unknown;
640 delete template[deprecatedKey];
641 }
642 }
643
644 /**
645 * Charging profiles should already be sorted by connector id and stack level (highest stack level has priority)
646 *
647 * @param chargingProfiles -
648 * @param logPrefix -
649 * @returns
650 */
651 private static getLimitFromChargingProfiles(
652 chargingProfiles: ChargingProfile[],
653 logPrefix: string
654 ): {
655 limit: number;
656 matchingChargingProfile: ChargingProfile;
657 } | null {
658 const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`;
659 const currentMoment = moment();
660 const currentDate = new Date();
661 for (const chargingProfile of chargingProfiles) {
662 // Set helpers
663 const chargingSchedule = chargingProfile.chargingSchedule;
664 if (!chargingSchedule?.startSchedule) {
665 logger.warn(
666 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not defined in charging profile id ${chargingProfile.chargingProfileId}`
667 );
668 }
669 // Check type (recurring) and if it is already active
670 // Adjust the daily recurring schedule to today
671 if (
672 chargingProfile.chargingProfileKind === ChargingProfileKindType.RECURRING &&
673 chargingProfile.recurrencyKind === RecurrencyKindType.DAILY &&
674 currentMoment.isAfter(chargingSchedule.startSchedule)
675 ) {
676 if (!(chargingSchedule?.startSchedule instanceof Date)) {
677 logger.warn(
678 `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: startSchedule is not a Date object in charging profile id ${chargingProfile.chargingProfileId}. Trying to convert it to a Date object`
679 );
680 chargingSchedule.startSchedule = new Date(chargingSchedule.startSchedule);
681 }
682 chargingSchedule.startSchedule.setFullYear(
683 currentDate.getFullYear(),
684 currentDate.getMonth(),
685 currentDate.getDate()
686 );
687 // Check if the start of the schedule is yesterday
688 if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
689 chargingSchedule.startSchedule.setDate(currentDate.getDate() - 1);
690 }
691 } else if (moment(chargingSchedule.startSchedule).isAfter(currentMoment)) {
692 return null;
693 }
694 // Check if the charging profile is active
695 if (
696 moment(chargingSchedule.startSchedule)
697 .add(chargingSchedule.duration, 's')
698 .isAfter(currentMoment)
699 ) {
700 let lastButOneSchedule: ChargingSchedulePeriod;
701 // Search the right schedule period
702 for (const schedulePeriod of chargingSchedule.chargingSchedulePeriod) {
703 // Handling of only one period
704 if (
705 chargingSchedule.chargingSchedulePeriod.length === 1 &&
706 schedulePeriod.startPeriod === 0
707 ) {
708 const result = {
709 limit: schedulePeriod.limit,
710 matchingChargingProfile: chargingProfile,
711 };
712 logger.debug(debugLogMsg, result);
713 return result;
714 }
715 // Find the right schedule period
716 if (
717 moment(chargingSchedule.startSchedule)
718 .add(schedulePeriod.startPeriod, 's')
719 .isAfter(currentMoment)
720 ) {
721 // Found the schedule: last but one is the correct one
722 const result = {
723 limit: lastButOneSchedule.limit,
724 matchingChargingProfile: chargingProfile,
725 };
726 logger.debug(debugLogMsg, result);
727 return result;
728 }
729 // Keep it
730 lastButOneSchedule = schedulePeriod;
731 // Handle the last schedule period
732 if (
733 schedulePeriod.startPeriod ===
734 chargingSchedule.chargingSchedulePeriod[
735 chargingSchedule.chargingSchedulePeriod.length - 1
736 ].startPeriod
737 ) {
738 const result = {
739 limit: lastButOneSchedule.limit,
740 matchingChargingProfile: chargingProfile,
741 };
742 logger.debug(debugLogMsg, result);
743 return result;
744 }
745 }
746 }
747 }
748 return null;
749 }
750
751 private static getRandomSerialNumberSuffix(params?: {
752 randomBytesLength?: number;
753 upperCase?: boolean;
754 }): string {
755 const randomSerialNumberSuffix = crypto
756 .randomBytes(params?.randomBytesLength ?? 16)
757 .toString('hex');
758 if (params?.upperCase) {
759 return randomSerialNumberSuffix.toUpperCase();
760 }
761 return randomSerialNumberSuffix;
762 }
763 }