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